Factory Girl building and creating 3 related models, association is lost after build stage - ruby

I have the following models
class Company
has_many :admins, class_name: 'Profile::CompanyAdmin'
validates :must_have_at_least_one_admin_validation
def must_have_at_least_one_admin_validation
errors.add(:admins, :not_enough) if admins.size.zero?
end
end
class Profile::CompanyAdmin
belongs_to :company
belongs_to :user, inverse_of: :company_admin_profiles
end
class User
has_many :company_admin_profiles, inverse_of: :user
end
I am trying to set up factories so I can easily build coherent data. Especially, I want to be able to create(:company, *traits) and it creates an Admin profile with a user account
factory :company do
transient do
# need one admin to pass validation
admins_count 1 # Admins with a user account
invited_admins_count 0 # Admins without user account
end
after(:build) do |comp, evaluator|
# Creating a company admin with a user
comp.admins += build_list(:company_admin_user,
evaluator.admins_count,
company: comp
).map { |u| u.company_admin_profiles.first }
comp.admins += build_list(:company_admin_profile,
evaluator.invited_admins_count,
company: comp
)
comp.entities = build_list(:entity,
evaluator.entity_count,
company: comp
)
# If I debug here, I have
# comp.admins.first.user # => Exists !
end
after(:create) do |comp, evaluator|
# If I debug here
# comp.admins.first.user # => Gone 😱
# First save users of admin profiles (we need the user ID for the admin profile user foreign key)
comp.admins.map(&:user).each(&:save!)
# Then save admins themselves
comp.admins.each(&:save!)
end
In the example above, when I debug at the end of the company after_build stage, I have successfully built admin profiles with thier users, however after the beginning of the after_create stage, I have lost the associated user in the admin profiles (cf comments)
What's wrong ?
For the reference here are the other factories for Profile/User
factory(:company_admin_user) do
transient do
company { build(:company, admins_count: 0) }
end
after(:build) do |user, evaluator|
user.company_admin_profiles << build(:company_admin_profile,
company: evaluator.company,
user: user,
)
end
after(:create) do |user, evaluator|
user.rh_profiles.each(&:save!)
end
end
factory :company_admin_profile, class: Profile::CompanyAdmin do
company
user nil # By default creating a CompanyAdmin profile does not create associated user
end
EDIT :
A simpler way to see the problem
company = FactoryGirl.build(:company)
company.admins.first.user # => Exists !
company.save # => true
company.admins.first.user # => Nil !

It would seem saving the company model first loses the 2-level deep nested user association. SO instead of
after(:create) do |comp, evaluator|
# First save the users
comp.admins.map(&:user).compact.each(&:save!)
# Then save admins themselves
comp.admins.each(&:save!)
The following does work (still not quite sure why though)
before(:create) do |comp, evaluator|
# Need to save newly created 2-level deep nested users first
comp.admins.map(&:user).compact.each(&:save!)
end
after(:create) do |comp, evaluator|
# Then save admins themselves
comp.admins.each(&:save!)
end

Related

Attempting to create a database item using the has_one relationship, no exceptions, but still no item

Models:
A User has_one Ucellar
A Ucellar belongs_to User
I have confirmed from multiple sources that these are set up correctly. For posterity, here is the top portion of those two models.
class User < ActiveRecord::Base
has_many :authorizations
has_one :ucellar
validates :name, :email, :presence => true
This is actually the entire Ucellar model.
class Ucellar < ActiveRecord::Base
belongs_to :user
end
Ucellar has a column called user_id, which I know is necessary. The part of my application that creates a user uses the method create_with_oath. Below is the entire User class. Note the second line of the create method.
class User < ActiveRecord::Base
has_many :authorizations
has_one :ucellar
validates :name, :email, :presence => true
def create
#user = User.new(user_params)
#ucellar = #user.create_ucellar
end
def add_provider(auth_hash)
# Check if the provider already exists, so we don't add it twice unless authorizations.find_by_provider_and_uid(auth_hash["provider"], auth_hash["uid"])
Authorization.create :user => self, :provider => auth_hash["provider"], :uid => auth_hash["uid"]
end
end
def self.create_with_omniauth(auth)
user = User.create({:name => auth["info"]["name"], :email => auth["info"]["email"]})
end
private
def user_params
params.require(:user).permit(:name, :email)
end
end
EDIT:
Forgot to summarize the symptoms. On create, the user is in the db, with no exceptions thrown, and nothing to signify that anything went wrong. However, the related ucellar is never created. Per the documentation Here, the create method should create AND save the related ucellar.
It should create ucellar too.
Try to get the error messages after the creation by calling:
raise #user.errors.full_messages.to_sentence.inspect
I'm not sure why this wasn't working, but I ended up just moving this code out of the create action of the user controller, and putting it directly after an action that was creating a user. It solved my issue though. Thanks everyone for your help!

FactoryGirl.create doesn't save Mongoid relations

I have two classes:
class User
include Mongoid::Document
has_one :preference
attr_accessible :name
field :name, type: String
end
class Preference
include Mongoid::Document
belongs_to :user
attr_accessible :somepref
field :somepref, type: Boolean
end
And I have two factories:
FactoryGirl.define do
factory :user do
preference
name 'John'
end
end
FactoryGirl.define do
factory :preference do
somepref true
end
end
After I create a User both documents are saved in the DB, but Preference document is missing user_id field and so has_one relation doesn't work when I read User from the DB.
I've currently fixed it by adding this piece of code in User factory:
after(:create) do |user|
#user.preference.save! #without this user_id field doesn't get saved
end
Can anyone explain to me why is this happening and is there a better fix?
Mongoid seems to be lacking support here.
When FactoryGirl creates a user, it first has to create the preference for that new user. As the new user does not have an id yet, the preference can't store it either.
In general, when you try create parent & child models in one operation, you need two steps:
create the parent, persist to database so it get's an id.
create the child for the parent and persist it.
Step two would end up in an after(:create) block. Like this:
FactoryGirl.define do
factory :user do
name 'John'
after(:create) do |user|
preference { create(:preference, user: user) }
end
end
end
As stated in this answer:
To ensure that you can always immediately read back the data you just
wrote using Mongoid, you need to set the database session options
consistency: :strong, safe: true
neither of which are the default.

How to work with an instance of a model without saving it to mongoid

The users of my Rails app are receiving a lot of emails (lets say they represent signups from new customers of my users). When an email is received a customer should be created, and the email should be saved as well. However, if the customer already exists (recognized by the email address of the email), the email email should not be saved to the database. I thought this was handled by Email.new, and then only save if the email address is recognized. But it seems that Email.new saves the record to the database. So how do I work with an email before actually deciding wether I want to save it?
Example code:
class Email
include Mongoid::Document
field :mail_address, type: String
belongs_to :user, :inverse_of => :emails
belongs_to :customer, :inverse_of => :emails
def self.receive_email(user, mail)
puts user.emails.size # => 0
email = Email.new(mail_address: mail.fetch(:mail_address), user: user) # Here I want to create a new instance of Email without saving it
puts user.emails.size # => 1
is_spam = email.test_if_spam
return is_spam if is_spam == true
is_duplicate = email.test_if_duplicate(user)
end
def test_if_spam
spam = true if self.mail_address == "spam#example.com"
end
def test_if_duplicate(user)
self.save
customer = Customer.create_or_update_customer(user, self)
self.save if customer == "created" # Here I want to save the email if it passes the customer "test"
end
end
class Customer
include Mongoid::Document
field :mail_address, type: String
belongs_to :user, :inverse_of => :customers
has_many :orders, :inverse_of => :customer
def self.create_or_update_customer(user, mail)
if user.customers.where(mail_address: mail.mail_address).size == 0
customer = mail.create_customer(mail_address: mail.mail_address, user: user)
return "created"
end
end
end
I'm going to suggest a somewhat fundamental reworking of your function. Try rewriting your function like this:
class Email
def self.save_unless_customer_exists(user, mail)
email = Email.new(
mail_address: mail.fetch(:mail_address),
user: user
)
return if email.customer or email.is_spam? or email.is_duplicate?
Customer.create!(user: user)
email.save!
end
end
You won't be able to drop that code in and expect it to work, because you'd have to define is_spam? and is_duplicate?, but hopefully you can at least see where I'm coming from.
I'd also recommend writing some automated tests for these functions if you haven't already. It will help you pin down the problem.

How to let Users edit a resource they create but not others when resource doesn't belong to User?

In my application using CanCan I have permissions where users can view and create stores but I also want them to only be able to edit the ones they've created. Users can create as many stores as they like, which all should be editable by them. A store doesn't have users so how could I do this when theirs no user_id apart of the Store table?
CanCan:
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new
if user.role == "default"
can :read, Store
can :create, Store
end
end
end
Since a user will be able to create as many stores as they like, a store will belong to a user.
You MUST create this relationship.
So, in the User model.
class User < ActiveRecord::Base
has_many :stores
end
And in the Store model.
class Store < ActiveRecord::Base
belongs_to :user
end
And in the ability.rb file, just put something like:
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user (not logged in)
if user.role == 'default'
can :manage, Store , :user_id => user.id
end
end
end
I would add the following to the store model:
has_one :created_by, :class => User
Then add a migration to add a created_by_id to your Store class.
You should then be able to add a CanCan::Ability:
can :edit, Store, :created_by => user
I agree with a previous poster, that you must set up a relationship between User and Store. The relationship can be one-to-many (as Kleber S. showed), or many-to-many, if a store can have multiple users.
Then, the best way to handle access control is in the controller, by using the association. For the show, edit, update, destroy methods, you'll need to find the store, given a logged in user, so do something like this:
class StoresController < ApplicationController
before_filter :find_store, only: [:show, :edit, :update, :destroy]
def show
end
def edit
end
def update
if #store.update_attributes(params[:store])
# redirect to show
else
# re-render edit, now with errors
end
end
# ...
private
def find_store
#store = current_user.stores.find(params[:id])
end
end
This way, the association takes care of limiting the lookup to only those stores that are connected to the current_user by foreign key. This is the standard way for RESTful Rails resources to perform access control of associated resources.

Find or create record through factory_girl association

I have a User model that belongs to a Group. Group must have unique name attribute. User factory and group factory are defined as:
Factory.define :user do |f|
f.association :group, :factory => :group
# ...
end
Factory.define :group do |f|
f.name "default"
end
When the first user is created a new group is created too. When I try to create a second user it fails because it wants to create same group again.
Is there a way to tell factory_girl association method to look first for an existing record?
Note: I did try to define a method to handle this, but then I cannot use f.association. I would like to be able to use it in Cucumber scenarios like this:
Given the following user exists:
| Email | Group |
| test#email.com | Name: mygroup |
and this can only work if association is used in Factory definition.
You can to use initialize_with with find_or_create method
FactoryGirl.define do
factory :group do
name "name"
initialize_with { Group.find_or_create_by_name(name)}
end
factory :user do
association :group
end
end
It can also be used with id
FactoryGirl.define do
factory :group do
id 1
attr_1 "default"
attr_2 "default"
...
attr_n "default"
initialize_with { Group.find_or_create_by_id(id)}
end
factory :user do
association :group
end
end
For Rails 4
The correct way in Rails 4 is Group.find_or_create_by(name: name), so you'd use
initialize_with { Group.find_or_create_by(name: name) }
instead.
I ended up using a mix of methods found around the net, one of them being inherited factories as suggested by duckyfuzz in another answer.
I did following:
# in groups.rb factory
def get_group_named(name)
# get existing group or create new one
Group.where(:name => name).first || Factory(:group, :name => name)
end
Factory.define :group do |f|
f.name "default"
end
# in users.rb factory
Factory.define :user_in_whatever do |f|
f.group { |user| get_group_named("whatever") }
end
You can also use a FactoryGirl strategy to achieve this
module FactoryGirl
module Strategy
class Find
def association(runner)
runner.run
end
def result(evaluation)
build_class(evaluation).where(get_overrides(evaluation)).first
end
private
def build_class(evaluation)
evaluation.instance_variable_get(:#attribute_assigner).instance_variable_get(:#build_class)
end
def get_overrides(evaluation = nil)
return #overrides unless #overrides.nil?
evaluation.instance_variable_get(:#attribute_assigner).instance_variable_get(:#evaluator).instance_variable_get(:#overrides).clone
end
end
class FindOrCreate
def initialize
#strategy = FactoryGirl.strategy_by_name(:find).new
end
delegate :association, to: :#strategy
def result(evaluation)
found_object = #strategy.result(evaluation)
if found_object.nil?
#strategy = FactoryGirl.strategy_by_name(:create).new
#strategy.result(evaluation)
else
found_object
end
end
end
end
register_strategy(:find, Strategy::Find)
register_strategy(:find_or_create, Strategy::FindOrCreate)
end
You can use this gist.
And then do the following
FactoryGirl.define do
factory :group do
name "name"
end
factory :user do
association :group, factory: :group, strategy: :find_or_create, name: "name"
end
end
This is working for me, though.
I had a similar problem and came up with this solution. It looks for a group by name and if it is found it associates the user with that group. Otherwise it creates a group by that name and then associates with it.
factory :user do
group { Group.find_by(name: 'unique_name') || FactoryBot.create(:group, name: 'unique_name') }
end
I hope this can be useful to someone :)
To ensure FactoryBot's build and create still behaves as it should, we should only override the logic of create, by doing:
factory :user do
association :group, factory: :group
# ...
end
factory :group do
to_create do |instance|
instance.id = Group.find_or_create_by(name: instance.name).id
instance.reload
end
name { "default" }
end
This ensures build maintains it's default behavior of "building/initializing the object" and does not perform any database read or write so it's always fast. Only logic of create is overridden to fetch an existing record if exists, instead of attempting to always create a new record.
I wrote an article explaining this.
I was looking for a way that doesn't affect the factories. Creating a Strategy is the way to go, as pointed out by #Hiasinho. However, that solution didn't work for me anymore, probably the API changed. Came up with this:
module FactoryBot
module Strategy
# Does not work when passing objects as associations: `FactoryBot.find_or_create(:entity, association: object)`
# Instead do: `FactoryBot.find_or_create(:entity, association_id: id)`
class FindOrCreate
def initialize
#build_strategy = FactoryBot.strategy_by_name(:build).new
end
delegate :association, to: :#build_strategy
def result(evaluation)
attributes = attributes_shared_with_build_result(evaluation)
evaluation.object.class.where(attributes).first || FactoryBot.strategy_by_name(:create).new.result(evaluation)
end
private
# Here we handle possible mismatches between initially provided attributes and actual model attrbiutes
# For example, devise's User model is given a `password` and generates an `encrypted_password`
# In this case, we shouldn't use `password` in the `where` clause
def attributes_shared_with_build_result(evaluation)
object_attributes = evaluation.object.attributes
evaluation.hash.filter { |k, v| object_attributes.key?(k.to_s) }
end
end
end
register_strategy(:find_or_create, Strategy::FindOrCreate)
end
And use it like this:
org = FactoryBot.find_or_create(:organization, name: 'test-org')
user = FactoryBot.find_or_create(:user, email: 'test#test.com', password: 'test', organization: org)
Usually I just make multiple factory definitions. One for a user with a group and one for a groupless user:
Factory.define :user do |u|
u.email "email"
# other attributes
end
Factory.define :grouped_user, :parent => :user do |u|
u.association :group
# this will inherit the attributes of :user
end
THen you can use these in your step definitions to create users and groups seperatly and join them together at will. For example you could create one grouped user and one lone user and join the lone user to the grouped users team.
Anyway, you should take a look at the pickle gem which will allow you to write steps like:
Given a user exists with email: "hello#email.com"
And a group exists with name: "default"
And the user: "hello#gmail.com" has joined that group
When somethings happens....
I'm using exactly the Cucumber scenario you described in your question:
Given the following user exists:
| Email | Group |
| test#email.com | Name: mygroup |
You can extend it like:
Given the following user exists:
| Email | Group |
| test#email.com | Name: mygroup |
| foo#email.com | Name: mygroup |
| bar#email.com | Name: mygroup |
This will create 3 users with the group "mygroup". As it used like this uses 'find_or_create_by' functionality, the first call creates the group, the next two calls finds the already created group.
Another way to do it (that will work with any attribute and work with associations):
# config/initializers/factory_bot.rb
#
# Example use:
#
# factory :my_factory do
# change_factory_to_find_or_create
#
# some_attr { 7 }
# other_attr { "hello" }
# end
#
# FactoryBot.create(:my_factory) # creates
# FactoryBot.create(:my_factory) # finds
# FactoryBot.create(:my_factory, other_attr: "new value") # creates
# FactoryBot.create(:my_factory, other_attr: "new value") # finds
module FactoryBotEnhancements
def change_factory_to_find_or_create
to_create do |instance|
# Note that this will ignore nil value attributes, to avoid auto-generated attributes such as id and timestamps
attributes = instance.class.find_or_create_by(instance.attributes.compact).attributes
instance.attributes = attributes.except('id')
instance.id = attributes['id'] # id can't be mass-assigned
instance.instance_variable_set('#new_record', false) # marks record as persisted
end
end
end
# This makes the module available to all factory definition blocks
class FactoryBot::DefinitionProxy
include FactoryBotEnhancements
end
The only caveat is that you can't find by nil values. Other than that, it works like a dream

Resources