I just studied ruby on rails today and I'd like some help on creating proper associations.
I have these models:
Comment:
class Comment < ActiveRecord::Base
belongs_to :stammr_post
validates :stammr_post_id, presence: true
validates :content, presence: true
end
Post:
class StammrPost < ActiveRecord::Base
has_many :comments, :dependent => :destroy
validates :content, presence: true
end
The thing is, whenever I create a Comment, and I enter a Stammr_post_id that doesn't exist, rails still accepts it as valid. Isn't that supposed to be invalid since a comment belongs to Stammr_post? The stammr_post should exist first before a comment could be made. How do I resolve this? Is it supposed to be automatic? Did I make a typo somewhere? Or do I need to do manual validation for that? Sorry, I'm kinda new to Ruby on Rails. I'm a former grails developer and I was used to the automatic associations thing. #_#
The correct way to do this is create the Comment through the parents association. That way you are taking advantage of the association;
So instead of doing this;
#comment = Comment.new(:stammr_post_id => 123)
#comment.save
do this;
# Find the StammrPost first. You may want to replace params[:stammr_post_id]
# with your StammrPost id
#stammr_post = StammrPost.find(params[:stammr_post_id])
#comment = #stammr_post.comments.build()
#comment.save
You could validate associated belongs_to object (stammr_post) instead of database column (stammr_post_id).
class Comment < ActiveRecord::Base
belongs_to :stammr_post
validates :stammr_post, :content, presence: true
end
Related
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!
I don't know what is the proper manner to ensure that there is only one record with the same couple of foreign keys. Let's assume a class named LineItem, which belongs to an cart and to item. Something like:
class LineItem < ActiveRecord::Base
belongs_to :cart
belongs_to :item
The point is: I want to make unique the combination of this two "attributes". At this time, my approach was using a after_save callback but I realized thats pretty poor. How do you resolve this? I was thinking in a PORO class (something like LineItemSaver) but I'm not completely convinced.
Thanks!
If I understood your question correctly,you want the scope option of the validates_uniqueness_of. If so, this should work.
In your LineItem model:
class LineItem < ActiveRecord::Base
belongs_to :cart
belongs_to :item
validates_uniqueness_of :cart_id, scope: :item_id
end
And also, you should be generating a migration to add this:
add_index :line_items, [ :cart_id, :item_id ], :unique => true
More Info here.
Can't you just do a uniqueness validation?
validates :cart_id, uniqueness: {scope: :item_id}
Try using a validate method to ensure that item_id is unique scoped to :cart_id
Something like:
validates :item_id, uniqueness: { scope: :cart_id }
Or if you want to fix the situation and simply increment the quantity you could also:
validate :ensure_line_item_uniqueness
def ensure_line_item_uniqueness
... whatever fixes the situation here ...
end
I have two models Post model and Comment model..First if a create a post it will have a post id of 1 then while creating a comment i can give association to the post using post_id equal to 1 but if i create a comment with post id of 2 which does not exist it would still go ahead and create a comment but with id of 'nil'..I want to ensure that the comment will be created only if the respective post_id is present.
class Post < ActiveRecord::Base
has_many :comments, dependent: destroy
end
class Comment < ActiveRecord::Base
belongs_to :post
validates_associated: post
end
As per my understanding validates_associated checks whether the validations in post model passes before creating a comment. Clarify me if i am wrong and what would be a appropriate solution for the above scenario?
First, the preferred way of setting the association b/w Post-Comment here is by :
def new
#product = Product.first
#comment = #product.comments.build
end
def create
#product = Product.find(params[:comment][:post_id])
#comment = #product.comments.create(comment_params)
end
For your particular scenario, I'm assuming that post_id is coming in params via some form or something, and then you wish to create a comment only if the post with that particular post_id exists. This can be done by adding following in Comment model:
validates :post, presence: true, allow_blank: false
OR
validate :post_presence, on: :create
def post_presence
errors.add(:post_id, "Post doesn't exist") unless Post.find(post_id).present?
end
You can even do the same thing at controller-side with before_action/before_filter hooks.
You can do this to validate the presence of post_id
class Comment < ActiveRecord::Base
belongs_to :post
validates :post_id, :presence => true
end
or to validate association, you can use
class Comment < ActiveRecord::Base
belongs_to :post
validates_presence_of :post
end
I am working on a self-learning Rails application (the source code can be found here. I want to validate the presence of the content before posting a text or an image:
.
Those are my models or look below:
class Post < ActiveRecord::Base
belongs_to :user
default_scope { order ("created_at DESC")}
belongs_to :content, polymorphic: true
has_reputation :votes, source: :user, aggregated_by: :sum
end
class PhotoPost < ActiveRecord::Base
has_attached_file :image, styles: {
post: "200x200>"
}
end
class TextPost < ActiveRecord::Base
attr_accessible :body
end
Here are my controllers in case they have a relation with this. Any other files can be found in my Github account. I am sure it will be messy to copy the whole project (that is why I am giving links for the controllers and for my project).
So what I have tried so far. (I tried those on the Posts Model)
=> Using validates_associated
validates_associated :content, :text_post
and getting an error "undefined method `text_post' for #Post:0x517c848>"
=> Used validates
validates :content, :presence => true
and getting no error however a post is created with no text.
validates :body, :presence => true
and getting an error "undefined method `body' for #Post:0x513e4a8>"
If you need any other information please let me know and I will provide it asap.
Thank you.
It would seem you have quite a confusing model setup with some key missing relation rules. E.g. Polymorphic rule which is not being utilised and a has_many relation between User and Post with no sign a of a user_id value in the Post model. Here is how I would set it up:
User.rb
def User << ActiveRecord::Base
has_many :text_posts
has_many :photo_posts
end
TextPost.rb
def TextPost << ActiveRecord::Base
attr_accessible :body, :user_id
belongs_to :user
validates :body, :presence => true
end
PhotoPost.rb
def PhotoPost << ActiveRecord::Base
attr_accessible :image, :user_id
belongs_to :user
validates :file, :presence => true, :format => {
:with => %r{\.(gif|png|jpg)$}i,
:message => "must be a URL for GIF, JPG or PNG image."
}
end
Then in your view you would need to do:
<%= form_for #text_post do |f| %>
# ...
<% end %>
And in your controller you can modify the create method to include the current_user from devise and assign it to the new text post record (user_id attribute):
text_posts_controller.rb
def create
#text_post = current_user.text_posts.new(params[:text_post])
end
This adheres more to the DRY principle which Ruby on Rails excels at - you shouldn't be writing alot of code to just create a new record.
I would advise on reading up on some Ruby on Rails standard and best practises. You shouldn't need to create a method in the Dashboard Model in order to create a new TextPost or PhotoPost record. This is a very confusing way of going about it; instead you should be utilising the power of ActiveRecord relation.
I would advise checking out Railscasts. They have alot of fulfilling content.
I'm still pretty new to testing in Rails 3, and I use RSpec and Remarkable. I read through a lot of posts and some books already, but I'm still kind of stuck in uncertainty when to use the association's name, when its ID.
class Project < ActiveRecord::Base
has_many :tasks
end
class Task < ActiveRecord::Base
belongs_to :project
end
Because of good practice, I want to protect my attributes from mass assignments:
class Task < ActiveRecord::Base
attr_accessible :project # Or is it :project_id??
belongs_to :project
end
First of all, I want to make sure that a project never exists without a valid task:
class Task < ActiveRecord::Base
validates :project, :presence => true # Which one is the...
validates :project_id, :presence => true # ...right way to go??
end
I also want to make sure that the assigned project or project ID is always valid:
class Task < ActiveRecord::Base
validates :project, :associated => true # Again, which one is...
validates :project_id, :associated => true # ...the right way to go?
end
...and do I need the validation on :presence when I use :associated??
Thanks a lot for clarifying, it seems that after hours of reading and trying to test stuff using RSpec/Shoulda/Remarkable I don't see the forest because of all the trees anymore...
This seems to be the right way to do it:
attr_accessible :project_id
You don't have to put :project there, too! It's anyway possible to do task.project=(Project.first!)
Then check for the existence of the :project_id using the following (:project_id is also set when task.project=(...) is used):
validates :project_id, :presence => true
Now make sure than an associated Project is valid like this:
validates :project, :associated => true
So:
t = Task.new
t.project_id = 1 # Value is accepted, regardless whether there is a Project with ID 1
t.project = Project.first # Any existing valid project is accepted
t.project = Project.new(:name => 'valid value') # A new valid project is accepted
t.project = Project.new(:name => 'invalid value') # A new invalid (or an existing invalid) project is NOT accepted!
It's a bit a pity that when assigning an ID through t.project_id = it's not checked whether this specific ID really exists. You have to check this using a custom validation or using the Validates Existence GEM.
To test these associations using RSpec with Remarkable matchers, do something like:
describe Task do
it { should validate_presence_of :project_id }
it { should validate_associated :project }
end
validates :project, :associated => true
validates :project_id, :presence => true
If you want to be sure that an association is present, you’ll need to
test whether the foreign key used to map the association is present,
and not the associated object itself.
http://guides.rubyonrails.org/active_record_validations_callbacks.html
attr_accessible :project_id
EDIT: assuming that the association is not optional...
The only way I can get it to thoroughly validate is this:
validates_associated :project
validates_presence_of :project_id,
:unless => Proc.new {|o| o.project.try(:new_record?)}
validates_presence_of :project, :if => Proc.new {|o| o.project_id}
The first line validates whether the associated Project is valid, if there is one. The second line insists on the project_id being present, unless the associated Project exists and is new (if it's a new record, it won't have an ID yet). The third line ensures that the Project is present if there is an ID present, i.e., if the associated Project has already been saved.
ActiveRecord will assign a project_id to the task if you assign a saved Project to project. If you assign an unsaved/new Project to project, it will leave project_id blank. Thus, we want to ensure that project_id is present, but only when dealing with a saved Project; this is accomplished in line two above.
Conversely, if you assign a number to project_id that represents a real Project, ActiveRecord will fill in project with the corresponding Project object. But if the ID you assigned is bogus, it will leave project as nil. Thus line three above, which ensures that we have a project if project_id is filled in -- if you supply a bogus ID, this will fail.
See RSpec examples that test these validations: https://gist.github.com/kianw/5085085
Joshua Muheim's solution works, but I hate being not be able to simply link a project to a task with an id like this:
t = Task.new
t.project_id = 123 # Won't verify if it's valid or not.
So I came up with this instead:
class Task < ActiveRecord:Base
belongs_to :project
validates :project_id, :presence => true
validate :project_exists
private
def project_exists
# Validation will pass if the project exists
valid = Project.exists?(self.project_id)
self.errors.add(:project, "doesn't exist.") unless valid
end
end