I have transferred my project from Rails 4 to Rails 6 and until recently, everything was working fine, but now when I go to update specific entries in my database, I get an error stating that "Boss must exist". The issue is there are a set of people that are the bosses but have no boss.
I have checked my schema and the field is not required. (boss_id)
person.rb
class Person < ApplicationRecord
belongs_to :boss, class_name: 'Person'
has_many :subordinates, class_name: 'Person', foreign_key: 'boss_id'
end
form.html.erb
<fieldset>
<label>
Boss
<%= f.select :boss_id, [[" ", :null], ["Name1", 1], ["Name2", 2], ... etc ... %>
</label>
</fieldset>
adminnamespace/people_controller.rb
class NameSpace::PeopleController < NameSpaceController
def index
#people = Person.all
end
def show
#person = Person.find(params[:id])
end
def new
#person = Person.new
end
def create
#person = Person.new(person_params)
if #person.save
flash[:notice] = "Person created successfully!"
redirect_to namespace_person_path(#person)
else
render :new, status: :unprocessable_entity
end
end
def edit
#person = Person.find(params[:id])
end
def update
#person = Person.find(params[:id])
if #person.update(person_params)
redirect_to namespace_person_path(#person)
else
render :edit, status: :unprocessable_entity
end
end
def destroy
person = Person.find(params[:id])
person.destroy
redirect_to namespace_people_path
end
private
def person_params
params.require(:person).permit(
:uname, ... :boss_id)
end
end
If the boss_id is set to 0 or a number that does not have a corresponding record, I get this error. Do I need a work around or is there a way to make this work as it did?
You should be able to add required: false to belongs_to statement.
Eg:
class Person < ApplicationRecord
belongs_to :boss, class_name: 'Person', required: false
has_many :subordinates, class_name: 'Person', foreign_key: 'boss_id'
end
Related
I have issue when create nested model in Rails 6:
post.rb
class Post < ApplicationRecord
has_many :post_votes, dependent: :destroy
end
post_vote.rb
class PostVote < ApplicationRecord
belongs_to :posts
end
routes.rb
resources :posts do
resources :post_votes
end
views:
<%= button_to post_post_votes_path(post), method: :post, remote: true, form_class: "post_vote" do%>
<%= bootstrap_icon "arrow-up-circle", width: 20, height: 20, fill: "#333" %>
<%end%>
PostVost Controller
def create
#post = Post.find(params[:post_id])
#post_vote = PostVote.new
if already_voted?
# flash[:notice] = "You can't vote more than once"
redirect_to root_path
else
#post_vote = #post.post_votes.build(user_id: current_user.id)
end
# redirect_to post_path(#post)
respond_to do |format|
format.html {}
format.js
end
end
def already_voted?
PostVote.where(user_id: current_user.id, post_id: params[:post_id]).exists?
end
I check the log file, no record was update in database
Any one known why i can not create new post_vote model?
Thank you so much!
On this line:
#post_vote = #post.post_votes.build(user_id: current_user.id)
.build only creates the object in memory. It does not persist it to the database.
Try:
#post_vote = #post.post_votes.create(user_id: current_user.id)
or
#post_vote = #post.post_votes.create!(user_id: current_user.id)
if you want an exception to be thrown if persistence fails.
The problem is using belong_to without optional
class PostVote < ApplicationRecord
belongs_to :posts
end
After:
class PostVote < ApplicationRecord
belongs_to :posts, optional: true
end
I have the following code letting a user to create a new album through a join table with an extra params (creator).
In order to do it, my controller does 2 requests (one for creating the album object and the collaboration object / the other to update the collaboration object with the extra params).
I would like to know if there is a way to do this call with only one request. (add the extra "creator" params in the same time than the album creation)
Thank you.
albums_controller.rb
class AlbumsController < ApplicationController
def new
#album = current_user.albums.build
end
def create
#album = current_user.albums.build(album_params)
if current_user.save
#album.collaborations.first.update_attribute :creator, true
redirect_to user_albums_path(current_user), notice: "Saved."
else
render :new
end
end
private
def album_params
params.require(:album).permit(:name)
end
end
Album.rb
class Album < ApplicationRecord
# Relations
has_many :collaborations
has_many :users, through: :collaborations
end
Collaboration.rb
class Collaboration < ApplicationRecord
belongs_to :album
belongs_to :user
end
User.rb
class User < ApplicationRecord
has_many :collaborations
has_many :albums, through: :collaborations
end
views/albums/new
= simple_form_for [:user, #album] do |f|
= f.input :name
= f.button :submit
You can just add associated objects on the new album instance:
#album = current_user.albums.new(album_params)
#album.collaborations.new(user: current_user, creator: true)
When you call #album.save ActiveRecord will automatically save the associated records in the same transaction.
class AlbumsController < ApplicationController
def new
#album = current_user.albums.new
end
def create
#album = current_user.albums.new(album_params)
#album.collaborations.new(user: current_user, creator: true)
if #album.save
redirect_to user_albums_path(current_user), notice: "Saved."
else
render :new
end
end
private
def album_params
params.require(:album).permit(:name)
end
end
You are also calling current_user.save and not #album.save. The former does work due to fact that it causes AR to save the associations but is not optimal since it triggers an unessicary update of the user model.
I'm trying to make that a subscriber, sub to an certain event
with the following url per example:
http://localhost:3001/events/1/subscribers/new
but I don't know how to associate event_id when creating a new subscriber
for the moment i'm getting this error:
Couldn't find Event with 'id'=
in the routes:
resources :events do
resources :subscribers #url/events/:events_id/subscribers/new
end
resources :events
root 'events#index'
in the subscribers controller:
def show
end
# GET /subscribers/new
def new
#puts "Look for me in console\n"
#puts params.inspect
#event = Event.find(params[:events_id])
#subscriber = #event.Subscriber.new
end
# GET /subscribers/1/edit
def edit
end
# POST /subscribers
# POST /subscribers.json
def create
#event = Event.find(params[:order_id])
#subscriber = #event.Subscriber.new order_params
##subscriber = Subscriber.new(subscriber_params)
respond_to do |format|
if #subscriber.save
SubsMailer.new_subscriber(#subscriber).deliver
format.html { redirect_to #subscriber, notice: 'Subscriber was successfully created.' }
format.json { render :show, status: :created, location: #subscriber }
else
format.html { render :new }
format.json { render json: #subscriber.errors, status: :unprocessable_entity }
end
end
end
in the new.html.erb:
<h1>New Subscriber</h1>
<%= render 'form', subscriber: #subscriber %>
<%= link_to 'Back', subscribers_path %>
model association:
event.rb:
class Event < ApplicationRecord
has_many :subscribers, dependent: :destroy
end
subscriber.rb:
class Subscriber < ApplicationRecord
belongs_to :event
validates :email, presence: true,
format: /\A\S+#\S+\z/,
uniqueness: { case_sensitive: false }
end
Well, I think this documentation will help you to understand what you need to do.
If briefly at first you need to change your models. You could have many to many for Event -> Subscriber association or one to many. One to many is the simplest to show so you need to add this to your Subscriber model:
belongs_to :event
And this to your Event model:
has_many :subscribers
Add new migration:
def change
remove_column :subscribers, :events_id
remove_column :subscribers, 'Event_id'
add_column :subscribers, :event_id, :integer
end
Then in your controller, you should change method calls, as Subscriber is a class, not the method.
def new
#event = Event.find(params[:event_id])
#subscriber = #event.subscribers.build
end
And you should be sure that in your database you have Event with this id.
To check it you can try to debug your controller code:
def new
puts "Event ids: " + Event.all.map(&:id).inspect
#event = Event.find(params[:event_id])
#subscriber = #event.subscribers.build
end
In your logs you should have something like:
Event ids: [1]
I think you just have a typo in your new method. You call params[:eventS_id] when it should be params[:event_id]. Also you don't properly reference your association. it should be event.subscribers.new:
def new
#puts "Look for me in console\n"
#puts params.inspect
#event = Event.find(params[:event_id])
#subscriber = #event.subscribers.build
end
Migration:
def up
change_table :subscribers do |t|
t.remove :Event_id
t.references :event
end
end
def down
change_table :subscribers do |t|
t.remove :event_id
t.add :Event_id, :integer
end
end
Keep me posted whether this helps and if you have any additional issues
I'm trying to create a form with a series of checks to prevent duplicates during the simultaneous creation of three model records: one for the parent (assuming it doesn't exist), one for its child (assuming it doesn't exist), and one for a join table between the child and the User (to allow the User to have their own copy of the Song object).
In the current state of the code, The checks seemingly pass, but
the server logs show ROLLBACK, and nothing gets saved
to the database EXCEPT the parent object (artist).
When I try to use the ids of the object, I get the error undefined method id for nil:NilClass, or "couldn't find object without an ID".
The following code is in my controller:
class SongsController < ApplicationController
before_action :authenticate_user!
def create
#artist = Artist.find_by(name: params[:artist][:name].strip.titleize) #look for the artist
#song = Song.find_by(title: params[:artist][:songs_attributes]["0"][:title].strip.titleize)
if #artist.present? && #song.present?
#user_song = current_user.user_songs.find(#song_id)
if #user_song.present?
render html: "THIS SONG IS ALREADY IN YOUR PLAYLIST"
render action: :new
else
#user_song = UserSong.create(user_id: current_user.id, song_id: #song.id)
redirect_to root_path
end
elsif #artist.present? && !#song.present?
#song = #artist.songs.build(title: params[:artist][:songs_attributes]["0"][:title].strip.titleize, lyrics: params[:artist][:songs_attributes]["0"][:lyrics].strip)
#user_song = UserSong.create(user_id: current_user.id, song_id: #song.id)
redirect_to root_path
elsif !#artist.present?
#artist = Artist.create(name: params[:artist][:name].strip.titleize)
#song = #artist.songs.build(title: params[:artist][:songs_attributes]["0"][:title].strip.titleize, lyrics: params[:artist][:songs_attributes]["0"][:lyrics].strip)
#user_song = UserSong.create(user_id: current_user.id, song_id: #song.id)
redirect_to root_path
else
render html: "SOMETHING WENT WRONG. CONTACT ME TO LET ME KNOW IF YOU SEE THIS MESSAGE"
end
end
def index
#songs = Song.all
end
def new
#artist = Artist.new
#artist.songs.build
#user_song = UserSong.new(user_id: current_user.id, song_id: #song_id)
end
def show
#song_id = params["song_id"]
#song = Song.find(params[:id])
end
def destroy
UserSong.where(:song_id => params[:id]).first.destroy
flash[:success] = "The song has been from your playlist"
redirect_to root_path
end
def edit
#song = Song.find(params[:id])
#artist = Artist.find(#song.artist_id)
end
def update
end
private
def set_artist
#artist = Artist.find(params[:id])
end
def artist_params
params.require(:artist).permit(:name, songs_attributes: [:id, :title, :lyrics])
end
def set_song
#song = Song.find(params["song_id"])
end
end
The models:
class Artist < ApplicationRecord
has_many :songs
accepts_nested_attributes_for :songs, reject_if: proc { |attributes| attributes['lyrics'].blank? }
end
class Song < ApplicationRecord
belongs_to :artist
has_many :user_songs
has_many :users, :through => :user_songs
end
class UserSong < ApplicationRecord
belongs_to :song
belongs_to :user
end
Sorry if I haven't abstracted enough. Not really sure how, given that there's no error message, just a rollback (without any validations present in any of the controllers).
Thanks to #coreyward and his pointing out of the fat-model skinny-controller lemma (never knew that was a thing), I was able to cut the code down and arrive at a solution immediately. In my models, I used validates_uniqueness_of and scope in order to prevent duplication of records. In my controller, I used find_or_create_by to seal the deal.
To whom it may concern, the final code is as follows:
class SongsController < ApplicationController
before_action :authenticate_user!
def create
#artist = Artist.find_or_create_by(name: params[:artist][:name].strip.titleize)
#song = #artist.songs.find_or_create_by(title: params[:artist][:songs_attributes]["0"][:title].strip.titleize) do |song|
song.lyrics = params[:artist][:songs_attributes]["0"][:lyrics].strip
end
#user_song = current_user.user_songs.find_or_create_by(song_id: #song.id) do |user_id|
user_id.user_id = current_user.id
end
redirect_to root_path
end
class Song < ApplicationRecord
validates_uniqueness_of :title, scope: :artist_id
belongs_to :artist
has_many :user_songs
has_many :users, :through => :user_songs
end
class Artist < ApplicationRecord
validates_uniqueness_of :name
has_many :songs
accepts_nested_attributes_for :songs, reject_if: proc { |attributes| attributes['lyrics'].blank? }
end
class UserSong < ApplicationRecord
validates_uniqueness_of :song_id, scope: :user_id
belongs_to :song
belongs_to :user
end
I'm having trouble having order go through. I have posted the error bellow. I think the issue has to do with the create method in the OrderController.rb, I do have the total_price method already defined but.. other than that I'm not sure how to fix the issue. Any help would be appreciated. Thank you.
class OrderTransaction
def initialize order, nonce
#order = order
#nonce = nonce
end
def execute
#result = Braintree::Transaction.sale(
amount: order.total_price,
payment_method_nonce: nonce
)
end
def ok?
#result.success?
end
private
attr_reader :order, :nonce
end
class Order < ActiveRecord::Base
belongs_to :user
has_many :order_items
def total_price
order_items.inject(0) { |sum, item| sum + item.total_price }
end
end
class OrdersController < ApplicationController
before_filter :initialize_cart
def index
#orders = Order.order(created_at: :desc).all
end
def create
#order_form = OrderForm.new(
user: User.new(order_params[:user]),
cart: #cart
)
if #order_form.save
notify_user
if charge_user
redirect_to root_path, notice: "Thank you for placing the order."
else
flash[:warning] = <<EOF
Your order ID is #{#order_form.order.id}.
<br/>
Something went wrong.
EOF
redirect_to new_payment_order_path(#order_form.order)
end
else
render "carts/checkout"
end
end
def update
#order = Order.find params[:id]
#previous_state = #order.state
if #order.update state_order_params
notify_user_about_state
redirect_to orders_path, notice: "Order was updated."
end
end
def new_payment
#order = Order.find params[:id]
#client_token = Braintree::ClientToken.generate
end
def pay
#order = Order.find params[:id]
transaction = OrderTransaction.new #order, params[:payment_method_nonce]
transaction.execute
if transaction.ok?
redirect_to root_path, notice: "Thank you for placing the order."
else
render "orders/new_payment"
end
end
private
def notify_user
#order_form.user.send_reset_password_instructions
OrderMailer.order_confirmation(#order_form.order).deliver
end
def notify_user_about_state
OrderMailer.state_changed(#order, #previous_state).deliver
end
def order_params
params.require(:order_form).permit(
user: [ :name, :phone, :address, :city, :country, :postal_code, :email ]
)
end
def charge_user
transaction = OrderTransaction.new #order, params[:payment_method_nonce]
transaction.execute
transaction.ok?
end
def state_order_params
params.require(:order).permit(:state)
end
end
class OrderItem < ActiveRecord::Base
belongs_to :order
belongs_to :product
def total_price
self.quantity * self.product.price
end
end
class OrderForm
include ActiveModel::Model
attr_accessor :user, :order # credit_card
attr_writer :cart
def save
set_password_for_user
if valid?
persist
true
else
false
end
end
def has_errors?
user.errors.any?
end
private
def valid?
user.valid?
end
def persist
user.save
#order = Order.create! user: user
build_order_items
end
def set_password_for_user
user.password = Digest::SHA1.hexdigest(user.email + Time.now.to_s)[0..8]
end
def build_order_items
#cart.items.each do |item|
#order.order_items.create! product_id: item.product_id, quantity: item.quantity
end
end
end
class OrderItem < ActiveRecord::Base
belongs_to :order
belongs_to :product
def total_price
self.quantity * self.product.price
end
end
As a standard note, any NilClass error basically means you haven't defined the variable you're trying to manipulate.
The key to solving the problem is to therefore find why the variable isn't defined, and populate it.
def execute
#result = Braintree::Transaction.sale(
amount: order.total_price,
payment_method_nonce: nonce
)
end
This is where Rails says the variable is not populated.
However, as with many problems in programming, the cause of the issue may not be as defined...
I initially thought the problem was that you weren't calling #order. However, the class initializes with order, so that shouldn't be a problem. So you have to look at how you're invoking the class:
transaction = OrderTransaction.new #order, params[:payment_method_nonce]
This surmises that #order is defined.
I surmise it isn't.
Here's what I'd do:
def create
#order_form = OrderForm.new(
user: User.new(order_params[:user]),
cart: #cart
)
if #order_form.save
notify_user
#order = #order_form.order #-> not efficient but should create #order
if charge_user
redirect_to root_path, notice: "Thank you for placing the order."
else
flash[:warning] = <<EOF
Your order ID is #{#order_form.order.id}.
<br/>
Something went wrong.
EOF
redirect_to new_payment_order_path(#order_form.order)
end
else
render "carts/checkout"
end
end
Personally, I think this highlights a deeper problem with your code structure:
You're creating an OrderForm object and yet processing #order_form.order
Your controller is full of tiny methods which bloat it up big time
Your controller is for orders, yet builds OrderForm objects
I'd do my best to make my controller as thin as possible:
#app/controllers/orders_controller.rb
class OrdersController < ApplicationController
def new
#order = current_user.order.new
end
def create
#order = current_user.order.new order_params
if #order.save
#order.charge
end
end
private
def order_params
params.require(:order).permit(:x, :y, :z, order_products_attributes: [:product, :qty])
end
end
I'd have a more modular model structure:
#app/models/order.rb
class Order < ActiveRecord::Base
belongs_to :user
has_many :order_products
has_many :products, through: :order_products, extend ProductQty
has_many :payments, inverse_of: :order
scope :cart, -> { order_products }
def total_price
products.pluck(:price, :qty) #-> need to work out
end
def charge
payment = payments.create
payment.execute ? payment.success : payment.error #-> something conditional
end
end
#app/models/order_product.rb
class OrderProduct < ActiveRecord::Base
#columns id | order_id | product_id | qty | created_at | updated_at
belongs_to :order
belongs_to :product
end
#app/models/payment.rb
class Payment < ActiveRecord::Base
belongs_to :order, inverse_of: :payments
def execute
Braintree::Transaction.sale(amount: order.total_price)
end
end
#app/models/product.rb
class Product < ActiveRecord::Base
has_many :order_products
has_many :orders, through: :order_products
end
#app/models/concerns/product_qty.rb
module ProductQty
#Load
def load
products.each do |qty|
proxy_association.target << qty
end
end
#Private
private
#Products
def products
return_array = []
through_collection.each_with_index do |through,i|
associate = through.send(reflection_name)
associate.assign_attributes({qty: items[i]})
return_array.concat Array.new(1).fill( associate )
end
return_array
end
#######################
# Variables #
#######################
#Association
def reflection_name
proxy_association.source_reflection.name
end
#Foreign Key
def through_source_key
proxy_association.reflection.source_reflection.foreign_key
end
#Primary Key
def through_primary_key
proxy_association.reflection.through_reflection.active_record_primary_key
end
#Through Name
def through_name
proxy_association.reflection.through_reflection.name
end
#Through
def through_collection
proxy_association.owner.send through_name
end
#Captions
def items
through_collection.map(&:qty)
end
#Target
def target_collection
proxy_association.target
end
end
I wanted to include cart somewhere, I'll have to do that another time.
For now, you'd be able to do the following:
#order = current_user.orders.find params[:id]
#order.products.each do |product|
product.qty #-> 5
#order.total_price #-> prices * qtys
--
This is not complete or tested, but I hope it shows you how you could improve your code structure dramatically, by making it modular. IE keep as many methods tied to your objects as possible.
In short, you should be able to do the following:
#order = current_users.orders.find params[:id]
if #order.payments.any?
#payment = #order.payment.first
#payment.success?
end
The problem is in your charge_user method inside OrdersController class where you call this code:
transaction = OrderTransaction.new #order, params[:payment_method_nonce]
you don't really defined #order in this method, i.e. #order is nil here and that's causing the problem for you here and you are getting this error: undefined method total_price for nil:NilClass
Set #order value inside the charge_user method before you call this line of code and make sure #order is NOT nil:
transaction = OrderTransaction.new #order, params[:payment_method_nonce]
One possible solution is to modify your charge_user method to take an order argument like this:
def charge_user(order)
transaction = OrderTransaction.new order, params[:payment_method_nonce]
transaction.execute
transaction.ok?
end
And, in your create method call like this:
if charge_user(#order_form.order)
redirect_to root_path, notice: "Thank you for placing the order."
else
# rest of the code
end
This will solve your issue.