FactoryGirl: why does attributes_for omit some attributes? - ruby-on-rails-3

I want to use FactoryGirl.attributes_for in controller testing, as in:
it "raise error creating a new PremiseGroup for this user" do
expect {
post :create, {:premise_group => FactoryGirl.attributes_for(:premise_group)}
}.to raise_error(CanCan::AccessDenied)
end
... but this doesn't work because #attributes_for omits the :user_id attribute. Here is the difference between #create and #attributes_for:
>> FactoryGirl.create(:premise_group)
=> #<PremiseGroup id: 3, name: "PremiseGroup_4", user_id: 6, is_visible: false, is_open: false)
>> FactoryGirl.attributes_for(:premise_group)
=> {:name=>"PremiseGroup_5", :is_visible=>false, :is_open=>false}
Note that the :user_id is absent from #attributes_for. Is this the expected behavior?
FWIW, my factories file includes definitions for :premise_group and for :user:
FactoryGirl.define do
...
factory :premise_group do
sequence(:name) {|n| "PremiseGroup_#{n}"}
user
is_visible false
is_open false
end
factory :user do
...
end
end

Short Answer:
By design, FactoryGirl's attribues_for intentionally omits things that would trigger a database transaction so tests will run fast. But you can can write a build_attributes method (below) to model all the attributes, if you're willing to take the time hit.
Original answer
Digging deep into the FactoryGirl documentation, e.g. this wiki page, you will find mentions that attributes_for ignores associations -- see update below. As a workaround, I've wrapped a helper method around FactoryGirl.build(...).attributes that strips id, created_at, and updated_at:
def build_attributes(*args)
FactoryGirl.build(*args).attributes.delete_if do |k, v|
["id", "created_at", "updated_at"].member?(k)
end
end
So now:
>> build_attributes(:premise_group)
=> {"name"=>"PremiseGroup_21", "user_id"=>29, "is_visible"=>false, "is_open"=>false}
... which is exactly what's expected.
update
Having absorbed the comments from the creators of FactoryGirl, I understand why attributes_for ignores associations: referencing an association generates a call to the db which can greatly slow down tests in some cases. But if you need associations, the build_attributes approach shown above should work.

I think this is a slight improvement over fearless_fool's answer, although it depends on your desired result.
Easiest to explain with an example. Say you have lat and long attributes in your model. On your form, you don't have lat and long fields, but rather lat degree, lat minute, lat second, etc. These later can converted to the decimal lat long form.
Say your factory is like so:
factory :something
lat_d 12
lat_m 32
..
long_d 23
long_m 23.2
end
fearless's build_attributes would return { lat: nil, long: nil}. While the build_attributes below will return { lat_d: 12, lat_m: 32..., lat: nil...}
def build_attributes
ba = FactoryGirl.build(*args).attributes.delete_if do |k, v|
["id", "created_at", "updated_at"].member?(k)
end
af = FactoryGirl.attributes_for(*args)
ba.symbolize_keys.merge(af)
end

To further elaborate on the given build_attributes solution, I modified it to only add the accessible associations:
def build_attributes(*args)
obj = FactoryGirl.build(*args)
associations = obj.class.reflect_on_all_associations(:belongs_to).map { |a| "#{a.name}_id" }
accessible = obj.class.accessible_attributes
accessible_associations = obj.attributes.delete_if do |k, v|
!associations.member?(k) or !accessible.include?(k)
end
FactoryGirl.attributes_for(*args).merge(accessible_associations.symbolize_keys)
end

Here is another way:
FactoryGirl.build(:car).attributes.except('id', 'created_at', 'updated_at').symbolize_keys
Limitations:
It does not generate attributes for HMT and HABTM associations (as these associations are stored in a join table, not an actual attribute).
Association strategy in the factory must be create, as in association :user, strategy: :create. This strategy can make your factory very slow if you don't use it wisely.

The accepted answer seems outdated as it did not work for me, after digging through the web & especially this Github issue, I present you:
A clean version for the most basic functionality for Rails 5+
This creates :belongs_to associations and adds their id (and type if :polymorphic) to the attributes. It also includes the code through FactoryBot::Syntax::Methods instead of an own module limited to controllers.
spec/support/factory_bot_macros.rb
module FactoryBot::Syntax::Methods
def nested_attributes_for(*args)
attributes = attributes_for(*args)
klass = args.first.to_s.camelize.constantize
klass.reflect_on_all_associations(:belongs_to).each do |r|
association = FactoryBot.create(r.class_name.underscore)
attributes["#{r.name}_id"] = association.id
attributes["#{r.name}_type"] = association.class.name if r.options[:polymorphic]
end
attributes
end
end
this is an adapted version of jamesst20 on the github issue - kudos to him 👏

Related

ActiveRecord: How to order and retrieve records in a greatest-n-per-group situation

I'm stuck with a classic greatest-n-per-group problem, where a cat can have many kittens, but I'm usually just interested in the youngest.
I already do know how to build a scope and a has_one relation for the Cat.
My question: Is there a way to...
list all cats' names together with their youngest kittens' names...
while at the same time ordering them by their respective youngest kitten's name...
...using just a single SELECT under the hood?
What I got so far:
class Cat < ApplicationRecord
has_many :kittens
has_one :youngest_kitten, -> { merge(Kitten.youngest) }, foreign_key: :cat_id, class_name: :Kitten
scope :with_youngest_kittens, lambda {
joins(:kittens)
.joins(Kitten.younger_kittens_sql("cats.id"))
.where(younger_kittens: { id: nil })
}
end
class Kitten
belongs_to :cat
scope :youngest, lambda {
joins(Kitten.younger_kittens_sql("kittens.cat_id"))
.where(younger_kittens: { id: nil })
}
def self.younger_kittens_sql(cat_field_name)
%{
LEFT OUTER JOIN kittens AS younger_kittens
ON younger_kittens.cat_id = #{cat_field_name}
AND younger_kittens.created_at > kittens.created_at
}
end
end
When I run Cat.with_latest_kittens.order('kittens.name').map(&:name) everything looks fine: I get all the cats' names with just a single SELECT.
But when I run Cat.with_latest_kittens.order('kittens.name').map {|cat| cat.youngest_kitten.name}, I get the right result too, but a superfluous additional SELECT per cat is executed. Which is just logical, because the with_youngest_kittens doesn't know it should populate youngest_kitten. Is there a way to tell it or am I going about this all wrong?
I think adding an includes to your :with_youngest_kittens scope will fix the problem. Try changing the scope to
scope :with_youngest_kittens, lambda {
includes(:youngest_kitten)
.joins(:kittens)
.joins(Kitten.younger_kittens_sql("cats.id"))
.where(younger_kittens: { id: nil })
}
This should prevent Rails from making a separate database query for every kitten.
I found a solution that produces no extra SELECT, however it is quite ugly, so I'll actually go for localarrow's solution as it's more readable!
I thought I'd still post it for the sake of completeness (If someone needs the few ms extra performance):
First I add custom tailored select fields for each kitten column to the Cat.with_youngest_kitten scope:
scope :with_youngest_kittens, lambda {
kitten_columns = Kitten
.column_names
.map { |column_name| "kittens.#{column_name} AS `youngest_kittens.#{column_name}`" }
.join(', ')
joins(:kittens)
.joins(Kitten.latest_outer_join_sql("cats.id"))
.where(later_kittens: { id: nil })
.select("cats.*, #{kitten_columns}")
}
Then I override the has_one youngest_kitten relation with a method, that retrieves those custom selects and calls super if no data has been retrieved:
def youngest_kitten
return super if self[:'youngest_kittens.id'].nil?
kitten_hash = Hash[Kitten.column_names.collect { |column_name| [column_name, self[:"youngest_kittens.#{column_name}"]] }]
kitten_hash[:cat] = self
Kitten.new(kitten_hash)
end

act_as_votable rails Counting votes on answers emited by specific user

I am close to new at Ruby on Rails (but love it) and I cannot managge to filter acuratelly.
Mi goal is to retrieve all the votes puted on answers created by one user. And I am using act_as_votable_gem
On answer.rb I have:
class Answer < ActiveRecord::Base
acts_as_votable
belongs_to :user
belongs_to :letter
...
end
At user.rb I have:
class User < ActiveRecord::Base
acts_as_voter
has_many :answers, dependent: :destroy
...
end
At users_controller.rb comes the trick because I have the letter form on the user show action:
def show
#user = User.find(params[:id])
#letter = Letter.new(params[:letter])
#letters = #user.letters.all
#answers = #user.answers
...
#emitedupvotes = #user.votes.up.count
#emiteddownvotes = #user.votes.down.count
#totalemitedvotes = #emitedupvotes + #emiteddownvotes
#receivedupvotes = ????????
#receiveddownvotes = ???????
...
end
I have tryed:
a) (the recomended one) #receivedupvotes = #answer.votes.up.count but this one needs #answer = Answer.find(params[:id]) and I am having => "ActiveRecord::RecordNotFound at /users/1 Couldn't find Answer with id=1"
Or, if I do: Answer.find(params[:answer_id]) => "Couldn't find Answer without an ID".
b) (the most logical) #receivedupvotes = #answers.votes.up.count => "undefined method `votes' for nil:NilClass"
c) (crazy one) #receivedupvotes = #answers.user.votes.up.count => "undefined method `user'"
d) #receivedupvotes = #answers.votes.up.where("voter = #user").count => "undefined method `votes' for nil:NilClass".
So, I tryed joins (never done before):
e) #receivedupvotes = #answers.joins(:votes).votes.up.count => "undefined method `votes' for..."
f) #receivedupvotes = #answers.joins(:votes).up.count => "undefined method `up' for..."
Any help? Thanks.
I don't know if you're still stuck on this, but if so..
What gets voted on? Seems like Answer gets voted on.
If User has_many :answers, then that means #user.answers will return you an array of Answer objects. However, judging by your point b) it looks like #answers is nil, so you'll have to dig into that to find out why.
So getting #user.answers to return an actual array of Answer and not nil is your first step.
Once you get that far, #answers.votes won't work because again, #answers is an array. Each Answer has votes, so you would call votes on an instance of Answer, not on the array.
Getting back to your main goal which is to tally up all the up and down votes a user received. Since User doesn't get voted on, but Answer does get voted on, you'll want to tally up all the votes for each of the user's answers.
Here's one way:
#receivedupvotes = 0
#receiveddownvotes = 0
#user.answers.each do |answer|
#receivedupvotes = #receivedupvotes + answer.votes.up
#receiveddownvotes = #receiveddownvotes + answer.votes.down
end
Also, you should really use underscores in your variable names, like #received_up_votes. It's the recommended way for Ruby and is more readable.
Jeff. Thank you very much. It is working now as it should.
This is the answer for others to read:
#user.answers is working as it should.
An this is the working implementation for counting votes on user answers with act_as_votable:
#user_answers_received_upvotes = 0
#user_answers_received_downvotes = 0
#user.answers.each do |answer|
#user_answers_received_upvotes = #user_answers_received_upvotes + answer.votes.up.size
#user_answers_received_downvotes = #user_answers_received_downvotes + answer.votes.down.size
end
#user_answers_total_received_votes = #user_answers_received_upvotes + #user_answers_received_downvotes
I will upvote your answer when I can.
Thanks again.

How do I clear a Model's :has_many associations without writing to the database in ActiveRecord?

For the sake of this question, let's say I have a very simple model:
class DystopianFuture::Human < ActiveRecord::Base
has_many :hobbies
validates :hobbies, :presence => {message: 'Please pick at least 1 Hobby!!'}
end
The problem is that when a human is updating their hobbies on a form and they don't pick any hobbies, there's no way for me to reflect this in the code without actually deleting all the associations.
So, say the action looks like this:
def update
hobbies = params[:hobbies]
human = Human.find(params[:id])
#ideally here I'd like to go
human.hobbies.clear
#but this updates the db immediately
if hobbies && hobbies.any?
human.hobbies.build(hobbies)
end
if human.save
#great
else
#crap
end
end
Notice the human.hobbies.clear line. I'd like to call this to make sure I'm only saving the new hobbies. It means I can also check to see if the user hasn't checked any hobbies on the form.
But I can't do that as it clears the db. I don't want to write anything to the database unless I know the model is valid.
What am I doing wrong here?
Initialy I also did this same way. Then found out one solution for this issue.
You need to do something like this
params[:heman][:hobby_ids]=[] if params[:human][:hobby_ids].nil?
Then check
if human.update_attributes(params[:human])
Hope you will get some idea...
EDIT:
Make hobbies params like this
hobbies = { hobbies_attributes: [
{ title: 'h1' },
{ title: 'h2' },
{ title: 'h3', _destroy: '1' } # existing hobby
]}
if Human.update_atttributes(hobbies) # use this condition
For this you need to declare accepts_nested_attributes_for :hobbies, allow_destroy: true in your Human model.
See more about this here http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
You can try https://github.com/ryanb/nested_form for this purpose..

Rails 3: As json with include option does not takes into account as_json redefinition for included association

I've got two models.
Class ModelA < ActiveRecord::Base
has_many :model_bs
end
Class ModelB < ActiveRecord::Base
belongs_to :model_a
def as_json(options = {})
{
:whatever => 'hello world'
}
end
end
When I call model_a.as_json(:include => :model_b), I want it to return a json which includes all model_bs, which it does, but employing my as_json redefinition, which it does not as it just uses the default one. Is there any way to use my own method rather than the original one? Thanks
In Rails 3, as_json method invokes serializable_hash to obtain the attributes hash. And they share the same 'options' parameter. In your case, overwritting serializable_hash would give the expected result.
def serializable_hash(options = {})
{:whatever => 'hello world'}
end
But, My suggestion is that instead of overwriting the convention, operate on the result of "super", which is like:
def serializable_hash(options = {})
hash = super
has[:name] = "hello world"
hash
end

Rails 3 polymorphic_path - how to change the default route_key

I got a config with Cart and CartItem (belongs_to :cart) models.
What I want to do is to call polymorphic_path([#cart, #cart_item]) so that it uses cart_item_path, instead of cart_cart_item_path.
I know I can change the url generated by the route to /carts/:id/items/:id, but that's not what I'm interested in. Also, renaming CartItem to Item is not an option. I just want to use cart_item_path method throughout the app.
Thanks in advance for any tip on that!
Just to make my point clear:
>> app.polymorphic_path([cart, cart_item])
NoMethodError: undefined method `cart_cart_item_path' for #<ActionDispatch::Integration::Session:0x007fb543e19858>
So, to repeat my question, what can I do in order for polymorphic_path([cart,cart.item]) to look for cart_item_path and not cart_cart_item_path?
After going all the way down the call stack, I came up with this:
module Cart
class Cart < ActiveRecord::Base
end
class Item < ActiveRecord::Base
self.table_name = 'cart_items'
end
def self.use_relative_model_naming?
true
end
# use_relative_model_naming? for rails 3.1
def self._railtie
true
end
end
The relevant Rails code is ActiveModel::Naming#model_name and ActiveModel::Name#initialize.
Now I finally get:
>> cart.class
=> Cart::Cart(id: integer, created_at: datetime, updated_at: datetime)
>> cart_item.class
=> Cart::Item(id: integer, created_at: datetime, updated_at: datetime)
>> app.polymorphic_path([cart, cart_item])
=> "/carts/3/items/1"
>> app.send(:build_named_route_call, [cart, cart_item], :singular)
=> "cart_item_url"
I think the same could work for Cart instead of Cart::Cart, with use_relative_model_naming? on the Cart class level.
You can declare the resources like this in your routes file.
resources :carts do
resources :cart_items, :as => 'items'
end
Refer to this section of the rails guide