We encountered strange behavior of FactoryGirl. Here is the definition of the commonx_log:
FactoryGirl.define do
factory :commonx_log, :class => 'Commonx::Log' do
log "My log"
resource_id 1
resource "MyString"
last_updated_by_id 1
end
end
Here is the validation in log model:
validates_presence_of :log, :resource, :resource_id
The following rspec would pass:
it "should be OK" do
c = FactoryGirl.build(:commonx_log, :last_updated_by_id => 2)
c.should be_valid
end
However as soon as we are trying to assign value to resource and resource_id:
c = FactoryGirl.build(:commonx_log, :resource => 'resource')
there is an error:
1) Commonx::Log should be OK
←[31mFailure/Error:←[0m ←[31mc.should be_valid←[0m
←[31mexpected #<Commonx::Log id: nil, log: "My log", resource_id: nil, resource: "resource", last_updated_by_id: 1, created_at: nil, updated_at: nil> to be valid
, but got errors: Resource can't be blank←[0m
What could cause the error? Is it resource key work in Factory Girl? Thanks for the help.
UPDATE:
The solution we have is to rename resource to resource_name in log model. After that, we can treat resource_name as regular field and do validation. When resource_id and resource appear in log model, rails assumes that resource is in certain type of association (see post by Ryan Bigg below). This assumption by rails automatically put resource_id and resource in validation and does not allow assigning value to resource_id (resource_id should be from association by default). This assumption causes problem in our app (can not assign resource_id) and we rename resource to break this tie of association.
The issue here is because you're validating the presence of the association. You don't need to do that at all. Remove resource from your validates_presence_of line.
Is there an actual case in your application where log entries can be created without resources? If not, I wouldn't be too concerned with the validations of these attributes. If you're super worried about resource_id being null, then placing a database constraint on resource_id would be the appropriate way to go.
resource is a rails keyword used for routing, perhaps that is what causes you trouble?
Related
How to validate uniqueness in model based on if other column has value or not, not on actual content? Scope seems to compare the content of the second column. For example I have in my User model columns email and project_id (both string) among others. I want to validate email to be unique if project_id is null or has any value. Using scope allows creating objects {email: 'a#a.a', project_id: nil}, {email: 'a#a.a', project_id: '1'}, {email: 'a#a.a', project_id: '2'} and so on. I want to limit the email uniqueness so that those first two objects would be possible (project_id is nil or 1 with same email) and last object would throw error 'email has already been taken' because there's already user with same email when project_id has value. Is there proper rails way to achieve that or do I need to write some custom validation?
Of course I have better email validation also and wouldn't accept 'a#a.a', that's just an example :)
The scope option in uniqueness validation is not the same as a Rails scope. It's more related to SQL and is generally restricted to just an attribute names.
So, since you can't use custom scope in uniqueness validation, you have to write custom validation for this purpose.
validate :validate_email_on_project_id_existence
def validate_email_on_project_id_existence
if User.where(email: self.email).where(self.project_id.nil? ? 'project_id IS NULL' : 'project_id IS NOT NULL').first
errors.add(:email, "some error")
end
end
You can create your custom validation and call it with validates_with, in your case it'd be something like:
class ExampleEmailUniqueness < ActiveModel::Validator
def validate(record)
error_message = "has already been taken"
if record.project_id.nil? && Example.where(project_id: nil, email: record.email).exists?
record.errors.add :email, error_message
elsif Example.where(email: record.email).where.not(project_id: nil).exists?
record.errors.add :email, error_message
end
end
end
class Example < ApplicationRecord
validates_with ExampleEmailUniqueness
end
That validation against your scenario would yield the results:
Example.new(email: 'a#a.a').save # Saved
Example.new(email: 'a#a.a', project_id: '1').save # Saved
Example.new(email: 'a#a.a', project_id: '2').save # Email has already been taken
Example.new(email: 'a#a.a').save # Email has already been taken
Note that saving a model with a nil project_id AFTER saving one that isn't nil will still pass validation and get saved to the database, which I'm not sure if it's the intended behavior.
In my Rails app, I am trying to save MAC addresses for devices belonging to different users. Each MAC address must be unique, so I included the uniqueness validation.
The validation itself seems to be working, since duplicate records were rejected when I tried using the Rails Console (ActiveRecord::RecordNotUnique). However, my test to check that only unique records can be saved is failing.
So my questions are:
Why is my test failing and how can I fix it?
I read elsewhere that the uniqueness validation alone is not a reliable way to assure uniqueness. Should I use other methods, such as before_save callbacks?
This is the error message I'm getting for the test:
Expected #<MacAddress id: nil, user_id: nil, address: "MACADD123", created_at: nil, updated_at: nil> to be nil or false
Setup in my model files:
# app/models/mac_address.rb
class MacAddress < ApplicationRecord
validates :address, uniqueness: true
belongs_to :user
end
# app/models/user.rb
class User < ApplicationRecord
has_many :mac_addresses, dependent: :destroy
end
Test to check for uniqueness:
class MacAddressTest < ActiveSupport::TestCase
test 'mac address must be unique' do
new_mac = 'MACADD123'
assert MacAddress.create(user: User.first, address: new_mac)
assert MacAddress.all.pluck(:address).include?(new_mac)
# The 'assert_not' below is failing.
assert_not MacAddress.create(user: User.second, address: new_mac)
end
end
Thanks for any help in advance.
As per the documentation on create:
Notice there's no id for that record. It hasn't been persisted. Check for errors with .errors.full_messages to see the uniqueness validation failure.
The resulting object is returned whether the object was saved successfully to the database or not.
You should assert that it's saved, like:
mac_address = MacAddress.create(...)
assert !mac_address.new_record?
Where that tells you if it's been saved or not. Alternatively you can use create! which will raise ActiveRecord::RecordInvalid if it failed.
For future reference and for anyone viewing this question - I rewrote my test with save instead of create like below:
test 'mac address must be unique' do
test_address = 'MACADD123'
original_mac = MacAddress.new(user: User.first, address: test_address)
duplicate_mac = MacAddress.new(user: User.second, address: test_address)
assert original_mac.save
assert MacAddress.pluck(:address).include?(test_address)
assert_not duplicate_mac.save
duplicate_mac.errors.messages[:address].include?('has already been taken')
end
I'm having trouble seeding my database using seed.rb, specifically where table relationships are concerned.
Here's a sample of the code:
# seed.rb
user = User.find_or_create_by_login(
:login => "myname",
:email => "myname#gmail.com",
:user_type => "Admin",
:password => "admin",
:password_confirmation => "admin")
project = Project.find_or_create_by_user_id(
:user_id => user.id,
:name => "Test Project")
When project is created (along with other unrelated parameters I've left out from above), user_id is empty. How can I get this to work?
This is the strangest behavior I've seen in something so simple. In my seed file, I have about eight tables being created and some are nested 3-4 levels deep (i.e. user has_many projects; projects has_many tasks, etc.).
When I call user user as above and reference user.id multiple times after that, it only works once! I tried adding [user.reload] before each new record is created but to no avail. I don't imagine this will make sense to anyone, but are there any possibilities here? Thanks all.
I figured out what the problem was. The fields that weren't populating were not listed explicitly in attr_accessible in their respective models. The fields listed were being saved correctly.
Thank you very much for your help everyone.
The code is fine and is the correct syntax for find_or_create. As others have said the most likely problem is that the user is invalid. Trying to call user.reload would make it blow up if the user is invalid and so will kind of make the problem more apparent, but the error you'll get from it will be useless (it'll moan about not being able to find a user without an id).
Unfortunately find_or_create doesn't work as a bang method to raise exceptions if it's invalid, so the best thing to do is probably raising an error and outputting the error after attempting to create the user:
user = User.find_or_create_by_login(:login => "myname")
raise "User is invalid: #{user.errors.full_messages}" unless user.valid?
User created with success? if so..try user.reload if not. that is probably the error
Are you sure your user is saved? I think the right syntax for find_or_create_by_XX is Blog.find_or_create_by_title("Some Blog"). If you need to pass more data you need to use find_or_initialize first and set other data after that separately.
Loosely related thread: Rails find_or_create by more than one attribute?
--edit
Passing data as hash to find_or_create_by_XX seems to work too. Docs are under "Dynamic attribute-based finders" here http://apidock.com/rails/v3.0.0/ActiveRecord/Base
try this
use User.find_or_create instead of User.find_or_create_by_login.
It seems like your user object is not saved.
Or before you assign user.id do user.reload
user = User.find_or_create(
:login => "myname",
:email => "myname#gmail.com",
:user_type => "Admin",
:password => "admin",
:password_confirmation => "admin")
[user.reload]
project = Project.find_or_create_by_user_id( :user_id => user.id,
:name => "Test Project")
Through rspec (I'm using rspec-1.3.0, rspec-rails-1.3.2 gems) generator (ruby script/generate rspec_model suggestion section_id:integer user_id:integer subject:string content:text state:string type:string) I created model and model spec and run rake db:migrate and rake:test:prepare
After that I started to work on my model spec:
require 'spec_helper'
describe Suggestion do
before(:each) do
#valid_attributes = {
:section_id => 1,
:user_id => 1,
:subject => 'Inappropriate title',
:content => 'The title of this section is inappropriate.',
:state => 'new',
:type => 'flag'
}
end
it "should create a new instance given valid attributes" do
Suggestion.create!(#valid_attributes)
end
it "should reject empty section_id attribute" do
empty_section_id_suggestion = Suggestion.new(#valid_attributes.merge(:section_id => ""))
empty_section_id_suggestion.should_not be_valid
end
...
Apart from 1st "should create a new instance given valid attributes" test I created 6 tests, basically each testing attribute of suggestion model for being empty - almost exactly same as "should reject empty section_id attribute" example.
When I run tests I get 6 failed tests, which is fine. First test "should create a new instance given valid attributes" passes.
Now when I go the the suggestion model and add validates_presence_of :all I get following error message related to 1st test:
ActiveRecord::RecordInvalid in 'Suggestion should create a new instance given valid attributes'
Validation failed: All can't be blank
./spec/models/suggestion_spec.rb:16:
When I try to run tests in isolation (validates_presence_of :attribute) all tests are passing, only with :type attribute I get again similar error message:
ActiveRecord::RecordInvalid in 'Suggestion should create a new instance given valid attributes'
Validation failed: Type can't be blank
./spec/models/suggestion_spec.rb:16:
I haven't encountered this problem before (have multiple similar models and their specs passing properly). It looks like it has problem with the :type attribute (it says it can't be empty), even I'm passing value to it through #valid_attributes. I tried to Google search but didn't find similar problem/solution.
Here is the test for :type attribute
it "should reject empty type attribute" do
empty_type_suggestion = Suggestion.new(#valid_attributes.merge(:type => ""))
empty_type_suggestion.should_not be_valid
end
Please check it out and let me know what I'm doing wrong here.
Thanks a lot for help
Peter
in your model you cant just say validate :all because :all isnt a column name.
class Suggestion < AR::Base
validates_pressence_of :subject, :content
end
there would be no reason to validate the presence of id columns but i guess you can if you want.
api documentation:
http://apidock.com/rails/ActiveModel/Validations/ClassMethods/validates_presence_of
So at the end I found the answer for the problem related to :type attribute:
http://www.gyrotechie.com/2008/09/activerecord-does-not-like-attributes-called-type/
The problem was that type is a reserved field name for classes that inherit from ActiveRecord.
I renamed the field name through migration and modified all related files and all is running properly now.
Say I have a basic Rails app with a basic one-to-many relationship where each comment belongs to an article:
$ rails blog
$ cd blog
$ script/generate model article name:string
$ script/generate model comment article:belongs_to body:text
Now I add in the code to create the associations, but I also want to be sure that when I create a comment, it always has an article:
class Article < ActiveRecord::Base
has_many :comments
end
class Comment < ActiveRecord::Base
belongs_to :article
validates_presence_of :article_id
end
So now let's say I'd like to create an article with a comment all at once:
$ rake db:migrate
$ script/console
If you do this:
>> article = Article.new
=> #<Article id: nil, name: nil, created_at: nil, updated_at: nil>
>> article.comments.build
=> #<Comment id: nil, article_id: nil, body: nil, created_at: nil, updated_at: nil>
>> article.save!
You'll get this error:
ActiveRecord::RecordInvalid: Validation failed: Comments is invalid
Which makes sense, because the comment has no page_id yet.
>> article.comments.first.errors.on(:article_id)
=> "can't be blank"
So if I remove the validates_presence_of :article_id from comment.rb, then I could do the save, but that would also allow you to create comments without an article id. What's the typical way of handling this?
UPDATE: Based on Nicholas' suggestion, here's a implementation of save_with_comments that works but is ugly:
def save_with_comments
save_with_comments!
rescue
false
end
def save_with_comments!
transaction do
comments = self.comments.dup
self.comments = []
save!
comments.each do |c|
c.article = self
c.save!
end
end
true
end
Not sure I want add something like this for every one-to-many association. Andy is probably correct in that is just best to avoid trying to do a cascading save and use the nested attributes solution. I'll leave this open for a while to see if anyone has any other suggestions.
I've also been investigating this topic and here is my summary:
The root cause why this doesn't work OOTB (at least when using validates_presence_of :article and not validates_presence_of :article_id) is the fact that rails doesn't use an identity map internally and therefore will not by itself know that article.comments[x].article == article
I have found three workarounds to make it work with a little effort:
Save the article before creating the comments (rails will automatically pass the article id that was generated during the save to each newly created comments; see Nicholas Hubbard's response)
Explicitly set the article on the comment after creating it (see W. Andrew Loe III's response)
Use inverse_of:
class Article < ActiveRecord::Base
has_many :comments, :inverse_of => :article
end
This last solution was bot yet mentioned in this article but seems to be rails' quick fix solution for the lack of an identity map. It also looks the least intrusive one of the three to me.
Instead of validating the presence of the article's id you could validate the presence of the article.
validates_presence_of :article
Then when you are creating your comment:
article.comments.build :article => article
You are correct. The article needs an id before this validation will work. One way around this is the save the article, like so:
>> article = Article.new
=> #<Article id: nil, name: nil, created_at: nil, updated_at: nil>
>> article.save!
=> true
>> article.comments.build
=> #<Comment id: nil, article_id: 2, body: nil, created_at: nil, updated_at: nil>
>> article.save!
=> true
If you are creating a new article with a comment in one method or action then I would recommend creating the article and saving it, then creating the comment, but wrapping the entire thing inside of a Article.transaction block so that you don't end up with any extra articles.
I fixed this problem adding this follow line to my _comment.html.erb:
"NEW" if form.object.new_record? %>
Now, the validation works in stand alone form, and in multi form too.
This fails because there is no identity map in Rails 2.3 or 3.0. You can manually fix it by stitching them together.
a = Article.new
c = a.comments.build
c.article = a
a.save!
This is horrible, and what the identity map in 3.1 will help to fix (in addition to performance gains). c.article and a.comments.first.article are different objects without an identity map.
If you're using Rails 2.3 you are using the new nested model stuff. I have noticed the same failures with validates_presence_of as you pointed out, or if :null => false was specified in the migration for that field.
If your intent is to create nested models, you should add accepts_nested_attributes_for :comments to article.rb. This would allow you to:
a = Article.new
a.comments_attributes = [{:body => "test"}]
a.save! # creates a new Article and a new Comment with a body of "test"
What you have is the way it should work to me, but I'm seeing that it doesn't with Rails 2.3.2. I debugged a before_create in comment using your code and no article_id is supplied through the build (it is nil) so this is not going to work. You'd need to save the Article first as Nicholas pointed out, or remove validation.