Can't validate children of a has_many through relation directly - ruby-on-rails

I have the following setup:
class Round < ActiveRecord::Base
has_many :cards_rounds
has_many :cards, through: :cards_rounds
accepts_nested_attributes_for :cards_rounds
validate :round_validations
private
def round_validations
// pry
unless cards.map(&:id).uniq.size == 3
errors.add(:round, "Must have 3 unique cards")
end
unless cards.map(&:quality).uniq.size == 1
errors.add(:round, "Cards must be of the same rarity")
end
end
end
class CardsRound < ActiveRecord::Base
belongs_to :card
belongs_to :round
end
class Card < ActiveRecord::Base
has_many :cards_rounds
has_many :rounds, through: :cards_rounds
end
Round always fails to validate on creation. When I step in using pry, I can see that cards is nil, but cards_rounds is populated and I can call cards_rounds[0].card (for example).
Is this the expected behaviour? It seems odd to me that I can reference the cards through cards_rounds but not directly as a collection.
Rails version is 4.0.1

Yeah I feel your pain. I'm not sure if this is the expected behavior but I've came into this same problem some times.
The best think you can do is to open an issue on https://github.com/rails/rails

Not sure where the problem is, I did the following gist which the tests pass: https://gist.github.com/arthurnn/9607180. See that I changed the validation. The problem is that the validation cannot rely on mapping the ids from cards, as they dont have an id yet.

Related

Rails: auto increment counter, scoped to a distant associated element

I have the following models:
Project
has_many :requirements
Requirement
belongs_to :project
has_many :issues
Issue
belongs_to :requirement
I need to have a counter on Issue that's scoped to the associated Project. Because Issue isn't associated directly to Project though, but only over the "bridge" of a Requirement, I can't simply use a gem like auto_increment.
So how should I do this? I see the following ways.
Idea 1: using an after_validation_on_create
class Issue
after_validation_on_create :increment_counter, if: -> { errors.empty? }
private
def increment_counter
if requirement
if requirement.project
self.counter = ... # Count all the findings + 1
end
end
end
end
This feels ugly to me though, as we have to make sure there's an associated requirement and project. Maybe there's a more beautiful way using an SQL query that fails gracefully if the associated objects are missing?
Update: I found the following to work: Project.joins(:findings).where(id: id).count. I'm not sure how Rails does this though (it automatically joins requirements).
Idea 2: associating finding to project
I could simply associate the the finding to the project, too:
Project
has_many :requirements
has_many :findings # New!
Requirement
belongs_to :project
has_many :issues
Issue
belongs_to :requirement
belongs_to :project # New!
This way I could use the auto_increment gem easily. But it feels a little redundant, as an issue already belongs to a project through the requirement in between. So the project_ids of both the issue and the requirement always need to have the same value, otherwise my data is corrupted, so I would have to make this sure somehow (but how?).
Both ideas don't feel "clean", they don't look "rails style" to me. There must be a better way! Any idea?
I have found out about has_many/has_one through, so it works the following way now:
Project
has_many :requirements
has_many :findings, through: :requirements
Requirement
belongs_to :project
has_many :issues
Issue
belongs_to :requirement
has_one :project, through: :requirement
Now I do the following:
class Finding < ActiveRecord::Base
before_create :increment_consecutive_number
private
def increment_consecutive_number
self.consecutive_number = project.findings.count + 1 if project
end
end

Testing Rails Deep Nested Attributes with RSPEC and FactoryGirl

I need some help getting my factory_girl settings correct on this has one through many with nested attributes. Here's the three models for reference.
location.rb
class Location < ActiveRecord::Base
has_many :person_locations
has_many :people, through: :person_locations
end
person_location.rb
class PersonLocation < ActiveRecord::Base
belongs_to :person
belongs_to :location
accepts_nested_attributes_for :location, reject_if: :all_blank
end
person.rb
class Person < ActiveRecord::Base
has_many :person_locations
has_many :locations, through: :person_locations
accepts_nested_attributes_for :person_locations, reject_if: :all_blank
end
Notice that locations is nested under the person record, but it needs to go through two models to be nested. I can get the tests working like this:
it "creates the objects and can be called via rails syntax" do
Location.all.count.should == 0
#person = FactoryGirl.create(:person)
#location = FactoryGirl.create(:location)
#person_location = FactoryGirl.create(:person_location, person: #person, location: #location)
#person.locations.count.should == 1
#location.people.count.should == 1
Location.all.count.should == 1
end
I should be able to create all three of these records within one line but haven't figured out how to yet. Here's the structure I would like to have work correctly :
factory :person do
...
trait :location_1 do
person_locations_attributes { location_attributes { FactoryGirl.attributes_for(:location, :location_1) } }
end
end
I have other models which are able to create via a similar syntax, but it has only one nested attribute versus this deeper nesting.
As entered above, I get the following error:
FactoryGirl.create(:person, :location_1)
undefined method `location_attributes' for #<FactoryGirl::SyntaxRunner:0x007fd65102a380>
Furthermore, I want to be able to test properly my controller setup for creating a new user with nested location. It will be tough to do this if I can't get the call down to one line.
Thanks for your help!! Hopefully I provided enough above to help others as well when they are creating a has many through relationship with nested attributes.
A couple of days later I figured it out after reading blog 1 and blog 2. In the process of refactoring all of my FactoryGirl code now.
FactoryGirl should look as follows:
factory :person do
...
trait :location_1 do
after(:create) do |person, evaluator|
create(:person_location, :location_1, person: person)
end
end
end
The person_location factory should be pretty straight forward then following the above code. You can either do the location_attributes which is in the original question or create a similar block to this answer to handle it there.

ActiveRecord query without *_id

I have 3 simple models:
class User < ActiveRecord::Base
has_many :subscriptions
end
class Product < ActiveRecord::Base
has_many :subscriptions
end
class Subscription < ActiveRecord::Base
belongs_to :user
belongs_to :product
end
I can do a_subscription.product = a_product and AR knows I mean product_id and everything works fine.
But If i do:
Subscription.where :product => a_product
It throws an error at me Unknown column 'subscriptions.product' - It knows in the first case that I mean product_id but it doesn't in the latter. I am just wondering if this is how it is suppose to be or am I missing something? I can get it to work by saying
Subscription.where :product_id => a_product
by do I have to specify _id?
Yes, right now you can't pass association to the where method. But you'll be able to do it in Rails 4. Here is a commit with this feature.
I don't think there's an elegant way around that (as of now, see #nash 's answer). However, if you have an instance of a_product and it has has_many on subscriptions, why not just turn it around and say:
subscriptions = a_product.subscriptions

How do I prevent my last nested attribute from being destroyed?

I have a customer model that has_many phones like so;
class Customer < ActiveRecord::Base
has_many :phones, as: :phoneable, dependent: :destroy
accepts_nested_attributes_for :phones, allow_destroy: true
end
class Phone < ActiveRecord::Base
belongs_to :phoneable, polymorphic: true
end
I want to make sure that the customer always has at least one nested phone model. At the moment I'm managing this in the browser but I want to provide a backup on the server (I've encountered a couple of customer records without phones and don't quite know how they got like that so I need a backstop).
I figure this must be achievable in the model but I'm not quite sure how to do it. All of my attempts so far have failed. I figure a before_destroy callback in the phone model is what I want but I don't know how to write this to prevent the destruction of the model. I also need it to allow the destruction of the model if the parent model has been destroyed. Any suggestion?
You can do it like this:
before_destory :check_phone_count
def check_phone_count
errors.add :base, "At least one phone must be present" unless phonable.phones.count > 1
end

Rails - insert many random items on create with has_many_through relation

I want to create a random pack of 15 cards which should be invoked in the cardpacks_controller on create. I have the following models:
Card:
class Card < ActiveRecord::Base
# relations
has_many :cardpacks, through: :cardpackcards
belongs_to :cardset
end
Cardpack:
class Cardpack < ActiveRecord::Base
#relations
has_many :cards, through: :cardpackcards
belongs_to :cardset
# accept attributes
accepts_nested_attributes_for :cards
end
Cardpackcards:
class Cardpackcard < ActiveRecord::Base
#relations
belongs_to :card
belongs_to :cardpack
end
Cardsets:
class Cardset < ActiveRecord::Base
#relations
has_many :cards
has_many :cardsets
end
How can I create 15 Cardpackcards records with random card_id values and with the same cardpack_id (so they belong to the same pack)
I have watched the complex form series tutorial but it gives me no comprehension as how to tackle this problem.
I hope anyone can help me solve this problem and give me more insight in the rails language.
Thanks,
Erik
Depending on the database system you might be able to use an order random clause to find 15 random records. For example, in Postgres:
Model.order("RANDOM()").limit(15)
Given the random models, you can add a before_create method that will setup the associations.
If the Cardpackcard model doesn't do anything but provide a matching between cards and cardpacks, you could use a has_and_belongs_to_many association instead, which would simplify things a bit.
Without it, the controller code might look something like this:
cardset = Cardset.find(params[:cardset_id])
cardpack = Cardpack.create(:cardset => cardset)
15.times do
cardpack.cardpackcards.create(:card => Card.create(:cardset => cardset))
end

Resources