Testing Rails Deep Nested Attributes with RSPEC and FactoryGirl - ruby-on-rails

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.

Related

Ruby on Rails: Is it possible to link a column with a column on another table?

I have models with deep associations in my Ruby on Rails API, sometimes 4 associations deep. For example:
Group has_many Subgroups has_many: Posts has_many: Comments
If I want to return Group.title with my comments, I need to say:
#comment.post.subgroup.group.title
Since this is way too many queries per Comment, I have added a column to the Comment table called group_title. This property is assigned when the Comment is created. Then every time the associated Group.title is updated, I have an after_update method on the Group model to update all associated Comment group_titles.
This seems like a lot of code to me and I find myself doing this often in this large scale app. Is there a way to link these 2 properties together to automatically update Comment.group_title every time its associated Group.title is updated?
I also had a similar relation hierarchy, and solved it (maybe there are better solutions) with joins.
Quarter belongs_to Detour belongs_to Forestry belongs_to Region
For a given detour, I find region name with one query.
Quarter.select("regions.name AS region_name, forestries.name as forestry_name, \
detours.name AS detour_name, quarters.*")
.joins(detour: [forestry: :region])
Sure, you can encapsulate it in a scope.
class Quarter
...
scope :with_all_parents, -> {
select("regions.name AS region_name, forestries.name as forestry_name, \
detours.name AS detour_name, quarters.*")
.joins(detour: [forestry: :region])
}
end
You can also use same approach.
class Comment
...
scope :with_group_titles, -> {
select("groups.title AS group_title, comments.*").joins(post: [subgroup: :group])
}
end
You can build hierarchies by using indirect associations:
class Group
has_many :subgroups
has_many :posts, through: :subgroups
has_many :comments, through: :posts
end
class Subgroup
belongs_to :group
has_many :posts
has_many :comments, through: :posts
end
class Post
belongs_to :subgroup
has_one :group, through: :subgroup
has_many :comments
end
class Comment
belongs_to :post
has_one :subgroup, through: :post
has_one :group, through: :post
end
The has_many :through Association
The has_one :through Association
This allows you to go from any end and rails will handle joining for you.
For example you can do:
#comment.group.title
Or do eager loading without passing a nested hash:
#comment = Comment.eager_load(:group).find(params[:id])
This however does not completely solve the performance issues related to joining deep nested hierarchies. This will still produce a monster of a join across four tables.
If you want to cache the title on the comments table you can use an ActiveRecord callback or you can define a database trigger procedure.
class Group
after_save :update_comments!
def update_comments!
self.comments.update_all(group_title: self.title)
end
end
You can do this by updating all the Comments from one side.
class Group
after_update do
Comment.joins(post: [subgroup: :group]).where("groups.title=?", self.title).update_all(group_title: self.title)
end
end

Confusion Abounds with FactoryGirl: has_many through

I am having a hard time figuring out what this error means exactly:
An error occurred in a `before(:suite)` hook.
Failure/Error: FactoryGirl.lint
FactoryGirl::InvalidFactoryError:
The following factories are invalid:
* question - Validation failed: Option must
exist, Question must exist (ActiveRecord::RecordInvalid)
These are the factories:
# question has many options through quiz
FactoryGirl.define do
factory :question, class: 'Question' do
option "What color are your eyes"
end
end
# option has many questions through quiz
FactoryGirl.define do
factory :option, class: 'Option' do
option "blue"
end
end
# JoinTable
FactoryGirl.define do
factory :quiz, class: 'Quiz' do
option nil
question nil
end
end
My guess is that the nil right next to the associations in Quiz factory has something to do with the error. I tried to read through FactoryGirl docs to see how to create the right associations but I do not understand certain things. For example:
am I supposed to create the associations in the factory for the join table (Question) only?
more importantly, since I want to first create a question and then tell the question what options it has, how do I achieve this in the Factory?
Thank you!
Edit:
Option Model:
class Option < ApplicationRecord
has_many :quizzes
has_many :questions, through: :quizzes
end
Question Model
class Question < ApplicationRecord
has_many :quizzes
has_many :options, through: :quizzes
end
Quiz Model
class Some::QuizQuestion < ApplicationRecord
belongs_to :questions
belongs_to :options
end
I think you need to add the class_name option to your relationships, I am pretty sure Rails doesn't deal with the Some namespace the way you're expecting it to.
For example:
class Some::QuizQuestion < ApplicationRecord
belongs_to :some_questions, class_name: 'Some::Question'
belongs_to :some_questions, through: :some_quiz_questions
end
Also, that second belongs_to doesn't make sense afaict, you are redefining some_questions right after defining it.
Forgot to post the solution to this question.
Main issue/question was:
why was the validation for the join table failing/returning error below:
* question - Validation failed: Option must
exist, Question must exist (ActiveRecord::RecordInvalid)
As I suspected, it was because of the nil value next to both of the associations in QuizQuestion factory:
FactoryGirl.define do
factory :quiz, class: 'Question' do
option nil
question nil
end
end
Solution was to get rid of the nil value.

How to fetch data through "has_many" associations?

I have the following structure of models:
class User < ActiveRecord::Base
has_many :favorites
end
class Favorite < ActiveRecord::Base
belongs_to :user
belongs_to :company
end
class Color < ActiveRecord::Base
belongs_to :company
end
class Company < ActiveRecord::Base
has_many :colors
has_many :favorities
end
It means that a company has many colors. Every user can favorite a company (and I can print out then every color that the respective company offers).
But I am trying to display all colors that companies I've favorited offers.
I've tried it something like this:
favorited_colors = current_user.favorites.colors
undefined method `colors' for #<ActiveRecord::Associations::CollectionProxy::ActiveRecord_Associations_CollectionProxy_Favorite:0x007fe037da01f0>
and
favorited_colors = current_user.favorites.companies.colors
undefined method `companies' for #<ActiveRecord::Associations::CollectionProxy::ActiveRecord_Associations_CollectionProxy_Favorite:0x007fe038ce8db0>
Is there any other way how to get a list of all colors from favorited companies than to iterated via each loops through all favorited companies and save all colors into an array?
Thank you
This should work from rails 3.1 on:
class User
has_many :favorites
has_many :companies, :through => :favorites
has_many :favorit_colors, :through => :companies
end
And then
favorited_colors = current_user.favorit_colors
This is not tested, though.
Collections
The problem you have is that you're calling the companies method on a collection object
The issue here is that you can only call methods on single instances of objects. This is demonstrated quite well with the error you're seeing:
undefined method `companies' for #<ActiveRecord::Associations::CollectionProxy::ActiveRecord_Associations_CollectionProxy_Favorite:0x007fe038ce8db0>
The way you should attempt to get this to work is to call the following:
current_user.favorites.each do |favourite|
= favourite.colors
end
I understand what you're looking to achieve, so let me give you some ideas:
Association Extensions
There's a functionality of Rails called ActiveRecord Association Extensions, which could give you the ability to provide what you're seeking:
#app/models/user.rb
class User < ActiveRecord::Base
has_many :favorites do
def colors
# Business logic for colours here
end
end
end
Now this is totally untested. However, I have experience with this deep part of Rails, and I therefore know that you'll have the following data-sets to work with:
proxy_association.owner returns the object that the association is a part of.
proxy_association.reflection returns the reflection object that describes the association.
proxy_association.target returns the associated object for belongs_to or has_one, or the collection of associated objects for
has_many or has_and_belongs_to_many.
From this, you'll be able to construct an array / object based off the colors you wish to show:
has_many :favorites do
def colors #totally untested
colors = {}
favorites = proxy_association.target
favorites.each do |favorite|
favorite.company.colors.each do |color|
colors << color
end
end
return colors
end
end

Rails 4 - Selecting associated models via collection (similar to .NET Linq's .SelectMany())

Forgive me if this has already been asked (as I believe it has), but I couldn't find this exact issue (and it's very likely I'm not searching properly).
I have the following models:
class Company < ActiveRecord::Base
has_many :jobs
end
class Job < ActiveRecord::Base
belongs_to :company
has_and_belongs_to_many :tags
end
class Tag < ActiveRecord::Base
has_and_belongs_to_many :jobs
end
What I'm trying to accomplish is a list of tags by company, but I'm failing to figure out how (I should note that I'm pretty new to Ruby and Rails). I come from a .NET background, and with Linq I'd use something like Company.Jobs.SelectMany(j => j.Tags).
I tried to do Company.first.jobs.tags, which fails with NoMethodError: undefined method 'tags' for #<Job::ActiveRecord_Associations_CollectionProxy:0x892dca0>, but strangely enough, if I run Company.first.jobs.instance_methods on a rails console, there is a :tags method. And this is what I get when I use the console's autocomplete:
Any suggestions?
Thanks.
What you are looking for is called 'has many through'.
class Company < ActiveRecord::Base
has_many :jobs
has_many :tags, through: :jobs
end
This is how you can go through an association (in this case jobs) to get to that association's association.
This way you can run company.tags on an instance of a company.
The problem with your existing approach is that jobs is a collection, but you need to hit the method on individual instances. This would work with no adjustment:
Company.first.jobs.collect { |job| job.tags }

Problem with nested associations in Rails3

I'm having a problem with some associations in Rails3.
I have the following associations
InformationCategory :has_many => Informations :has_many => InformationValues
I'm able to succesfully do the following:
Information.first.information_values
=> [#InformationValue..]
I'm also able to do the following:
InformationCategory.first.informations
=> [#Information...]
However, for some reason, this fails:
InformationCategory.first.informations.first.information_values
=> NoMethodError: undefined method `information_values' for #<Information:0x000001053321c8>
Why can't I use 'nested associations' in Rails? The error message clearly states that InformationCategory.first.informations.first returns an instance of Information
Am I doing something wrong?
Thanks in advance!
You don't define all the nested descendants on outermost model: each model defines what it "directly" has_many of, or the model it belongs_to. Because your question is wrong, I can only guess without seeing more specifically how your models are supposed to be related.
This might be a start:
class InformationCategory < ActiveRecord::Base
has_many :informations
end
class Information < ActiveRecord::Base
belongs_to :information_category
has_many :information_values
end
class InformationValue < ActiveRecord::Base
belongs_to :information
end
However, you might be trying to do a has_many :through, but I can't tell from your question.

Resources