Does Rails allow conditional validations for validates_associated? I'm seeing the following on Rails 4.2.0. Am I trying to do it incorrectly?
Models:
class User < ActiveRecord::Base
has_many :books
validates_associated :books, if: :should_validate_book?
def should_validate_book?
return false
end
end
class Book < ActiveRecord::Base
belongs_to :user
validates_presence_of :title
end
The presence validation on Book's title attribute still runs (Rails console):
> u = User.create!
=> #<User id: 2, created_at: "2015-02-24 19:34:51", updated_at: "2015-02-24 19:34:51">
> u.books.build
=> #<Book id: nil, user_id: 3, title: nil, created_at: nil, updated_at: nil>
> u.valid?
=> false
> u.books.first.errors
=> #<ActiveModel::Errors:0x007fa256b210d8 #base=#<Book id: nil, user_id: 3, title: nil, created_at: nil, updated_at: nil>, #messages={:title=>["can't be blank"]}>
It turns out that validates_associated is ON by default for has_many relationships. To make it conditionally, you'd need to add validate: false to the has_many declaration:
has_many :books, validate: false
In Rails since time immemorial validates_associated has only taken a list of attributes. Besides that, you'd kind of be mixing up behavior between your models from what I gather from the criteria you've pasted. A better approach would be to adjust your validations in the Book model to account for the variation and let Book decide for itself whether an object should be validated or not.
Related
I have an issue with inserting data via rails command. Bellow you can see my model and the issue. The title is displaying nil even tho I created a new instance of Post with the title hash. I am aware you can do this in a different way. I am using this simple example to figure out why can't I insert or display data from the database.
Model
category.rb
class Category < ApplicationRecord
attr_accessor :name
has_many :posts
end
post.rb
class Post < ApplicationRecord
attr_accessor :title, :body, :category_id, :author_id
belongs_to :category
end
Rails c
post = Post.new(:title => 'Test')
=> #<Post id: nil, title: nil, body: nil, category_id: nil, author_id: nil, created_at: nil, updated_at: nil>
You should not be using the attr_accessor in your Rails class. Rails automatically make these attributes readable, and you should generally only be writing by saving records to the database.
I have the following models in my schema:
class Notification < ApplicationRecord
has_many :impacts, as: :impactable
accepts_nested_attributes_for :impacts, reject_if: :all_blank, allow_destroy: true
end
class Impact < ApplicationRecord
#association
belongs_to :impactable, polymorphic: true
#enum
enum impact_type: [:full_school, :standard, :section]
end
Whenever I try to save a notification now - I get the an error - Validation failed: Impacts impactable must exist
I've tried to create impacts from notifications manually with Notification.last.impacts.create and they work fine.
What could be the problem here?
More info -
When I add a byebug to the #notification object before it saves in the controller - this is the output -
>> #notification
=> #<Notification id: nil, notification_type: "email", title: "Test", content: "Tomorrow is a holiday", created_at: nil, updated_at: nil>
And also checking for it's associations --
>> #notification.impacts
=> #<ActiveRecord::Associations::CollectionProxy [#<Impact id: nil, impact_type: "standard", standard: 2, created_at: nil, updated_at: nil, impactable_id: nil, impactable_type: "Notification", section: "">]>
You just need to add inverse_of: :impactable to your Notifications Model.
has_many :impacts, as: :impactable, inverse_of: :impactable
I have an object that looks like this in Rails console:
pry(#<User>)> favorite_foods
=> [#<FavoriteFood id: 3, food_id: 1, user_id: 1, removed: false, created_at: "2015-09-06 00:49:35", updated_at: "2015-09-06 00:49:35">,
#<FavoriteFood id: 4, food_id: 2, user_id: 1, removed: true, created_at: "2015-09-06 12:16:56", updated_at: "2015-09-06 12:17:36">]
favorite_foods is in a polymorphic relationship with foods.
has_many :favorite_foods
has_many :favorites, through: :favorite_foods, source: :food
has_many :foods, through: :favorite_foods
In the favorite_food.rb model, I'm setting favorite foods by using this method:
def favorited_foods
favorite_foods.map(&:food_id).to_set
end
I need to be able to remove all instances of favorite_foods where the field "removed" equals true.
For example: from the console session above, I want favorite_foods to include food_id 3, not food_id 4.
What is a clean way to do this in the favorited_foods method? I tried incorporating .reject and it ignored it.
Thanks for any advice!
You can just simply do:
FavoriteFood.where(removed: true).delete_all
which will delete all the FavoriteFood records where removed is true.
If you want to do it using map, then you can do:
favorited_foods.select { |f| f.removed == true }.map { |f| f.delete }
but this is not recommended as it will call delete method once for each record. The better way is deleting them all using delete_all as shown above.
Can't you do something like:
def remove_favorited_foods
favorite_foods.each {|food| Food.destroy(food) if food.removed?}
end
You should also add has_many :foods, dependent: :destroy, through: :favorite_foods to your model to ensure that the favorite foods are destroyed accordingly. See this guide.
To wrap up a exercise I'm working on, I'm trying to test an association through a join table. What I'd like to do is to test the association that Artists have many collections through Art Pieces and Collections have many Artists through Art Pieces. Below is my code:
context 'associations' do
let(:artist){ Artist.create(first_name: "Daniel", last_name: "Rubio",
email: 'drubs#email.com', birthplace: 'Mexico',
style: 'Contemporary') }
let(:art_piece) { ArtPiece.new(date_of_creation: DateTime.parse('2012-3-13'),
placement_date_of_sale: DateTime.parse('2014-8-13'),
cost: 250, medium: 'sculpture', availability: true, artist_id:
artist.id, customer_id: customer.id,
collection_id: collection.id)}
let(:customer) { Customer.create(first_name: 'Carmen', last_name: 'Dent', email:
'cdent#email.com', no_of_purchases: 10, amount_spent: 100000) }
let(:collection) { Collection.create(name: 'Romanticism') }
it 'has many collections through art pieces' do
expect(artist).to respond_to(:collections)
art_piece.save!
expect(artist.collections).to eql(collection)
end
end
Now I'm positive my associations are setup correctly on my validations:
class Collection < ActiveRecord::Base
validates_presence_of :name
has_many :art_pieces
has_many :artists, through: :art_pieces
end
class Artist < ActiveRecord::Base
validates_presence_of :first_name
validates_presence_of :last_name
validates :email, presence: true, uniqueness: true
validates_presence_of :style
validates_presence_of :birthplace
has_many :art_pieces
has_many :collections, through: :art_pieces
end
I followed activerecord's guidelines and it makes sense to me. The only two things that I think is happening is either A. I have a syntax error somewhere or B. my variables are not being chained properly. Any insight?
Below is the error message I get:
1) Artist associations has many collections through art pieces
Failure/Error: expect(artist.collections).to eql(collection)
expected: #<Collection id: 20, name: "Romanticism", created_at: "2014-04-06 16:57:42", updated_at: "2014-04-06 16:57:42">
got: #<ActiveRecord::Associations::CollectionProxy [#<Collection id: 20, name: "Romanticism", created_at: "2014-04-06 16:57:42", updated_at: "2014-04-06 16:57:42">]>
(compared using eql?)
Diff:
## -1,2 +1,2 ##
-#<Collection id: 20, name: "Romanticism", created_at: "2014-04-06 16:57:42", updated_at: "2014-04-06 16:57:42">
+[#<Collection id: 20, name: "Romanticism", created_at: "2014-04-06 16:57:42", updated_at: "2014-04-06 16:57:42">]
artist.collection returns array-like Relation, while you test it to return single ActiveRecord object. You should have:
expect(artist.collections).to eql([collection])
in your test.
Try changing
expect(artist.collections).to eql(collection)
to
expect(artist.collections).to eql([collection])
Currently your rspec is expecting a single collection object but what your query returns is an array like object with a single element in it. Changing the expectation to a list of your collection should do the trick.
I have a very odd mass assignment error that shows up when I use association methods to create new objects.
I have a user model that looks like this:
class User < ActiveRecord::Base
has_many :posts, :dependent => :destroy
end
I also have a posts model that looks like this:
class Post < ActiveRecord::Base
belongs_to :user
attr_accessible :body, :title
end
If I do the following in console, I get a mass assignment warning:
> user = User.create(:name => "Daniel");
> user.posts.create(:title => "Hello World")
=> #<Post id: 1, body: nil, title: "Hello World", created_at: "2011-11-03
18:24:06", updated_at "2011-11-03 18:24:06", user_id = 1>
> user.posts
=> WARNING: Can't mass-assign attributes: created_at, updated_at, user_id
When I run user.posts again, however, I get:
> user.posts
=> [#<Post id: 1, body: nil, title: "Hello World", created_at: "2011-11-03
18:24:06", updated_at "2011-11-03 18:24:06", user_id = 1>]
There are a couple of other tricks I can do to avoid the mass assignment error, such as calling user.posts before I do users.posts.create.
Why is this happening and how can I prevent it?
I'm using Rails 3.0.7.
How about changing your user model to include attr_accessible for posts association
class User < ActiveRecord::Base
has_many :posts, :dependent => :destroy
attr_accessible :posts
end