I am building a simple expenses management app on rails 5.1.4. I am using the following five models.
Payees
class Payee < ApplicationRecord
has_many :expenses
validates :title, uniqueness: true, presence: true
end
Accounts
class Account < ApplicationRecord
before_save :update_balance
validates :balance, numericality: { greater_than_or_equal_to: 0 }
has_many :expenses
end
Budgets
class Budget < ApplicationRecord
belongs_to :categories
has_many :expenses, through: :categories
end
Categories
class Category < ApplicationRecord
validates :title, uniqueness: true, presence: true
has_many :expenses
has_one :budget
end
Expenses
class Expense < ApplicationRecord
belongs_to :categories
belongs_to :budgets
belongs_to :payees
belongs_to :accounts
validates :title, :value, presence: true
before_save :default_account
end
When I try to create a new expense I am facing a validation error
Validation failed: Categories must exist, Budgets must exist, Payees must exist, Accounts must exist
The issue is that all the above records exist. To explain my self let's say I am passing the params account_id: 1, payee_id: 1, category_id: 1. If I do:
Account.find(1) #=> Finds the record
Category.find(1) #=> also ok
Payee.find(1) #=> also ok
I am aware of the solution referred in this question (adding optional: true) but I don't get why I should do that while all of the above exist
Edit
The code that is raising the error is:
def create
#expense = Expense.create!(title: params[:expense]['title'],
value: params[:expense]['value'],
date: params[:expense]['date'],
comment: params[:expense]['comment'],
payee_id: params[:expense]['payee_id'],
category_id: params[:expense]['category_id'],
account_id: params[:expense]['account_id'])
end
The parameters that are passed through the form are
{"utf8"=>"✓",
"authenticity_token"=>"DWd1HEcBC3DhUahfOQcdaY0/oE+VHapxxE+HPUb0I6iSiqMxkz6l+vlK+1zhb66HnZ/vZRUVG4ojTdWUCjHtGg==",
"expense"=>{"title"=>"test", "value"=>"-20", "category_id"=>"1", "payee_id"=>"2", "date"=>"2018-01-21", "account_id"=>"1", "comment"=>""},
"commit"=>"Submit"}
I would first start by commenting out all your model validations, then creating an expense. Add back one model validation at a time, each time test creating an expense to see what validation is causing the error.
also you may want to change how you're creating the expense to something like below.
change your controllers create action to
def create
#expense = Expense.new(expense_params)
if #expense.save
flash[:success] = "expense created"
redirect_to expense_url(#expense.id)
else
render 'new'
end
end
next under your private method at the bottom of your controller you want to do something like this
private
# Never trust parameters from the scary internet, only allow the white list through.
def expense_params
params.require(:expense).permit(:title, :value, :date, etc...)
end
I finally found out where the problem is! It was the naming of the classes/models that raised the error. I had named my models on singular (Account, Category, etc) while all references are searching for plurals ( Accounts, Categories, etc). I had to re-do all migrations from the very beginning in order to make it work the proper way!
Thanks to everyone for spending the time though!
Related
Creating a record with nested associations fails to save any of the associated records if one of the records fails validations.
class Podcast < ActiveRecord::Base
has_many :episodes, inverse_of: :podcast
accepts_nested_attributes_for :episodes
end
class Episode < ActiveRecord::Base
belongs_to :podcast, inverse_of: :episodes
validates :podcast, :some_attr, presence: true
end
# Creates a podcast with one episode.
case_1 = Podcast.create {
title: 'title'
episode_attributes: [
{title: "ep1", some_attr: "some_attr"}, # <- Valid Episode
]
}
# Creates a podcast without any episodes.
case_2 = Podcast.create {
title: 'title'
episode_attributes: [
{title: "ep1", some_attr: "some_attr"}, # <- Valid Episode
{title: "ep2"} # <- Invalid Episode
]
}
I'd expect case_1 to save successfully with one created episode.
I'd expect case_2 to do one of two things:
Save with one episode
Fail to save with validation errors.
Instead the podcast saves but neither episode does.
I'd like the podcast to save with any valid episode saved as well.
I thought to reject invalid episodes by changing the accepts nested attributes line to
accepts_nested_attributes_for :episodes, reject_if: proc { |attributes| !Episode.new(attributes).valid? }
but every episode would be invalid because they don't yet have podcast_id's, so they would fail validates :podcast, presence: true
Try this pattern: Use the :reject_if param in your accepts_nested_attributes_for directive (docs) and pass a method to discover if the attributes are valid. This would let you offload the validation to the Episode model.
Something like...
accepts_nested_attributes_for :episodes, :reject_if => :reject_episode_attributes?
def reject_episode_attributes?( attributes )
!Episode.attributes_are_valid?( attributes )
end
Then in Episode you make a method that tests those however you like. You could even create a new record and use existing validations.
def self.attributes_are_valid?( attributes )
new_e = Episode.new( attributes )
new_e.valid?
end
You can use validates_associated to cause the second option (Fail to save with validation errors)
class Podcast < ActiveRecord::Base
has_many :episodes, inverse_of: :podcast
validates_associated :episodes
accepts_nested_attributes_for :episodes
end
UPDATE:
To do option one (save with one episode) you could do something like this:
1. Add the validates_associated :episodes
2. Add code in your controller's create action after the save of #podcast fails. First, inspect the #podcast.errors object to see if the failure is caused by validation error of episodes (and only that) otherwise handle as normal. If caused by validation error on episodes then do something like #podcast.episodes.each {|e| #podcast.episodes.delete(e) unless e.errors.empty?} Then save again.
This would look something like:
def create
#podcast = Podcast.new(params[:podcast])
if #podcast.save
redirect_to #podcast
else
if #some conditions looking at #podcast.errors to see that it's failed because of the validates episodes
#podcast.episodes.each do |episode|
#podcast.episodes.delete(episode) unless episode.errors.empty?
end
if #podcast.save
redirect_to #podcast
else
render :new
end
else
render :new
end
end
end
To get the first option, try turning autosaving on for your episodes.
class Podcast < ActiveRecord::Base
has_many :episodes, inverse_of: :podcast, autosave: true
accepts_nested_attributes_for :episodes
end
Terribly worded, but I'm confusing it.
I have a User model who has_many Clients and has_many statements, through: :clients and then statements which belongs_to clients and belongs to user
In Console I can do all the queries I want. User.statements User.client.first.statements etc - What I'm struggling on is Controller restrictions
For now it's simple - A user should only be able to see Clients and Statements in which they own.
For Clients I did
Client Controller
def index
#clients = Client.where(user_id: current_user.id)
end
Which seems to work perfectly. Client has a field for user_id
I'm kind of stuck on how to emulate this for Statements. Statements do -not- have a user_id field. I'm not quite sure I want them too since in the very-soon-future I want clients to belongs_to_many :users and Statements to not be bound.
Statement Controller
def index
#clients = Client.where(user_id: current_user.id)
#statements = Statement.where(params[:client_id])
end
I'm just genuinely not sure what to put - I know the params[:client_id] doesn't make sense, but what is the proper way to fulfill this? Am I going about it an unsecure way?
Client Model
class Client < ApplicationRecord
has_many :statements
has_many :client_notes, inverse_of: :client
belongs_to :user
validates :name, presence: true
validates :status, presence: true
accepts_nested_attributes_for :client_notes, reject_if: :all_blank, allow_destroy: true
end
Statement Model
class Statement < ApplicationRecord
belongs_to :client
belongs_to :user
validates :name, presence: true
validates :statement_type, presence: true
validates :client_id, presence: true
validates :start_date, presence: true
validates :end_date, presence: true
end
User Model
class User < ApplicationRecord
has_many :clients
has_many :statements, through: :clients
end
Based on the reply provided below I am using
def index
if params[:client][:user_id] == #current_user.id
#clients = Client.includes(:statements).where(user_id: params[:client][:user_id])
#statements = #clients.statements
else
return 'error'
end
end
Unsure if this logic is proper
Use includes to avoid [N+1] queries.
And regarding "A user should only be able to see Clients and Statements in which they own".
if params[:client][:user_id] == #current_user.id
#clients = Client.includes(:statements).where(user_id: params[:client][:user_id])
# do more
else
# Type your error message
end
Additionally, you might need to use strong params and scope.
The best way to do it is using includes:
#clients = Client.where(user_id: current_user.id)
#statements = Statement.includes(clients: :users}).where('users.id = ?', current_user.id)
You can take a look in here: https://apidock.com/rails/ActiveRecord/QueryMethods/includes
In this case, thanks to the reminder that current_user is a helper from Devise, and the relational structure I showed, it was actually just as simple as
def index
#statements = current_user.statements
end
resolved my issue.
Due to the [N+1] Queries issue that #BigB has brought to my attention, while this method works, I wouldn't suggest it for a sizable transaction.
I want to change has_many association behaviour
considering this basic data model
class Skill < ActiveRecord::Base
has_many :users, through: :skills_users
has_many :skills_users
end
class User < ActiveRecord::Base
has_many :skills, through: :skills_users, validate: true
has_many :skills_users
end
class SkillsUser < ActiveRecord::Base
belongs_to :user
belongs_to :skill
validates :user, :skill, presence: true
end
For adding a new skill we can easily do that :
john = User.create(name: 'John Doe')
tidy = Skill.create(name: 'Tidy')
john.skills << tidy
but if you do this twice we obtain a duplicate skill for this user
An possibility to prevent that is to check before adding
john.skills << tidy unless john.skills.include?(tidy)
But this is quite mean...
We can as well change ActiveRecord::Associations::CollectionProxy#<< behaviour like
module InvalidModelIgnoredSilently
def <<(*records)
super(records.to_a.keep_if { |r| !!include?(r) })
end
end
ActiveRecord::Associations::CollectionProxy.send :prepend, InvalidModelIgnoredSilently
to force CollectionProxy to ignore transparently adding duplicate records.
But I'm not happy with that.
We can add a validation on extra validation on SkillsUser
class SkillsUser < ActiveRecord::Base
belongs_to :user
belongs_to :skill
validates :user, :skill, presence: true
validates :user, uniqueness: { scope: :skill }
end
but in this case adding twice will raise up ActiveRecord::RecordInvalid and again we have to check before adding
or make a uglier hack on CollectionProxy
module InvalidModelIgnoredSilently
def <<(*records)
super(valid_records(records))
end
private
def valid_records(records)
records.with_object([]).each do |record, _valid_records|
begin
proxy_association.dup.concat(record)
_valid_records << record
rescue ActiveRecord::RecordInvalid
end
end
end
end
ActiveRecord::Associations::CollectionProxy.send :prepend, InvalidModelIgnoredSilently
But I'm still not happy with that.
To me the ideal and maybe missing methods on CollectionProxy are :
john.skills.push(tidy)
=> false
and
john.skills.push!(tidy)
=> ActiveRecord::RecordInvalid
Any idea how I can do that nicely?
-- EDIT --
A way I found to avoid throwing Exception is throwing an Exception!
class User < ActiveRecord::Base
has_many :skills, through: :skills_users, before_add: :check_presence
has_many :skills_users
private
def check_presence(skill)
raise ActiveRecord::Rollback if skills.include?(skill)
end
end
Isn't based on validations, neither a generic solution, but can help...
Perhaps i'm not understanding the problem but here is what I'd do:
Add a constraint on the DB level to make sure the data is clean, no matter how things are implemented
Make sure that skill is not added multiple times (on the client)
Can you show me the migration that created your SkillsUser table.
the better if you show me the indexes of SkillsUser table that you have.
i usually use has_and_belongs_to_many instead of has_many - through.
try to add this migration
$ rails g migration add_id_to_skills_users id:primary_key
# change the has_many - through TO has_and_belongs_to_many
no need for validations if you have double index "skills_users".
hope it helps you.
Creating a record with nested associations fails to save any of the associated records if one of the records fails validations.
class Podcast < ActiveRecord::Base
has_many :episodes, inverse_of: :podcast
accepts_nested_attributes_for :episodes
end
class Episode < ActiveRecord::Base
belongs_to :podcast, inverse_of: :episodes
validates :podcast, :some_attr, presence: true
end
# Creates a podcast with one episode.
case_1 = Podcast.create {
title: 'title'
episode_attributes: [
{title: "ep1", some_attr: "some_attr"}, # <- Valid Episode
]
}
# Creates a podcast without any episodes.
case_2 = Podcast.create {
title: 'title'
episode_attributes: [
{title: "ep1", some_attr: "some_attr"}, # <- Valid Episode
{title: "ep2"} # <- Invalid Episode
]
}
I'd expect case_1 to save successfully with one created episode.
I'd expect case_2 to do one of two things:
Save with one episode
Fail to save with validation errors.
Instead the podcast saves but neither episode does.
I'd like the podcast to save with any valid episode saved as well.
I thought to reject invalid episodes by changing the accepts nested attributes line to
accepts_nested_attributes_for :episodes, reject_if: proc { |attributes| !Episode.new(attributes).valid? }
but every episode would be invalid because they don't yet have podcast_id's, so they would fail validates :podcast, presence: true
Try this pattern: Use the :reject_if param in your accepts_nested_attributes_for directive (docs) and pass a method to discover if the attributes are valid. This would let you offload the validation to the Episode model.
Something like...
accepts_nested_attributes_for :episodes, :reject_if => :reject_episode_attributes?
def reject_episode_attributes?( attributes )
!Episode.attributes_are_valid?( attributes )
end
Then in Episode you make a method that tests those however you like. You could even create a new record and use existing validations.
def self.attributes_are_valid?( attributes )
new_e = Episode.new( attributes )
new_e.valid?
end
You can use validates_associated to cause the second option (Fail to save with validation errors)
class Podcast < ActiveRecord::Base
has_many :episodes, inverse_of: :podcast
validates_associated :episodes
accepts_nested_attributes_for :episodes
end
UPDATE:
To do option one (save with one episode) you could do something like this:
1. Add the validates_associated :episodes
2. Add code in your controller's create action after the save of #podcast fails. First, inspect the #podcast.errors object to see if the failure is caused by validation error of episodes (and only that) otherwise handle as normal. If caused by validation error on episodes then do something like #podcast.episodes.each {|e| #podcast.episodes.delete(e) unless e.errors.empty?} Then save again.
This would look something like:
def create
#podcast = Podcast.new(params[:podcast])
if #podcast.save
redirect_to #podcast
else
if #some conditions looking at #podcast.errors to see that it's failed because of the validates episodes
#podcast.episodes.each do |episode|
#podcast.episodes.delete(episode) unless episode.errors.empty?
end
if #podcast.save
redirect_to #podcast
else
render :new
end
else
render :new
end
end
end
To get the first option, try turning autosaving on for your episodes.
class Podcast < ActiveRecord::Base
has_many :episodes, inverse_of: :podcast, autosave: true
accepts_nested_attributes_for :episodes
end
I am building an application that requires users to schedule appointment/conversations times with mentors. I am having a hard time building this out. I have been reading up on the has_many :through => Association but I know I am going about this the wrong way.
In my User.rb
Class User < ActiveRecord::Base
has_many :mentor_requests, foreign_key: "user_id"
has_many :mentors, through: :mentor_requests
def requested?(mentor)
mentor_requests.find_by_mentor_id(mentor.id)
end
def request!(mentor_request)
mentor_requests.create!(mentor_request)
end
def unrequest!(mentor)
mentor_requests.find_by_mentor_id(mentor.id).destroy
end
end
In my Mentor.rb
class Mentor < User
has_many :mentor_requests, foreign_key: "mentor_id"
has_many :users, through: :mentor_requests
end
In the Mentor_request.rb
class MentorRequest < ActiveRecord::Base
attr_accessible :reason, :mentor_id
belongs_to :user, class_name: "User"
belongs_to :mentor, class_name: "Mentor"
validates :user_id, :mentor_id, presence: true
validates :reason, presence:true, length: { maximum: 140 }
default_scope order: 'mentor_requests.created_at DESC'
end
In my requests controller is
def create
#mentor_request = current_user.mentor_requests.build(params[:mentor_request])
#current_user.request!(#mentor)
if #mentor_request.save
flash[:success] = "Your request has been sent"
redirect_to user_path(current_user)
#Send confirmations to both user and mentor
#Send the notification to an internal message inbox
else
render "new"
end
end
When I go to the view at mentor_requests/new.html.erb and try to submit the request it says that the mentor_id has to be present and the content has to be present. I tried creating a request using a modal view from the mentor show page but the content doesnt save and I validate that the presence has to be true, and then when it redirects to mentor_requests/new.html.erb the mentor id is no longer present.
I do not know if I have presented enough information but I seriously need help here. If I am on the right path what do I need to do to get it work, and if all this is wrong what do I do to get what I want.
Thanks a lot
Jude
make your mentor_request route nested under mentor. Since a mentor request doesn't make sense without the context of a mentor this is the perfect spot for a nested resource.
resources :mentors do
resources :mentor_requests
end
this will make your route something like /mentors/1/mentor_requests
And then in your controller you will also have a params[:mentor_id] so make it
def create
#mentor_request = current_user.mentor_requests.build(params[:mentor_request])
#mentor_request.mentor = Mentor.find(params[:mentor_id])
end
As for your content error, it seems like that is a field required on mentor_request that you arent' filling in. You'll need to pass that back too and assign it to the mentor_request or just take off the validation if you dont' need it