Seeding with unique objects - ruby-on-rails

I'm trying to seed my database with project with unique project name, however my seeder does not work as I intended.
Seed.rb
users = User.order(:created_at).take(6)
50.times do |n|
name = "project-#{n+1}"
category = "category-#{n+1}"
users.each { |user| user.projects.create!(name: name, category: category) }
end
If I remove validates :name, presence: true, uniqueness: true it will create 50 projects for each user from 1 to 50, but then for the next user would do the same (count resets) and will create projects with titles from 1 to 50 which interferes with the validates rule.
Any ideas?

You can get last project_id and initial counter with it. Too user.id as additional scope.
last_id = Project.last.try(:id) || 1
50.times do |n|
name = "project-#{last_id+n}"
category = "category-#{last_id+n}"
users.each do |user|
user.projects.create!(name: ("#{name}-#{user.id}"), category: ("#{category}-#{user.id}"))
end
end
Too you can add rand, Time.now.to_f.to_s, SecureRandom.hex(5)

Related

How to create an instance variable with FactoryBot on Rspec

I want to test a class function on RSpec on my Product class. To ease the reading I will keep it as this:
class Product < ApplicationRecord
private
def self.order_list(sort_by)
if sort_by == 'featured' || sort_by.blank?
self.order(sales: :desc)
elsif sort_by == 'name A-Z'
self.order(name: :asc)
elsif sort_by == 'name Z-A'
self.order(name: :desc)
elsif sort_by == 'rating 1-5'
self.order(:rating)
elsif sort_by == 'rating 5-1'
self.order(rating: :desc)
elsif sort_by == 'price low-high'
self.order(:price)
elsif sort_by == 'price high-low'
self.order(price: :desc)
elsif sort_by == 'date old-new'
self.order(:updated_at)
elsif sort_by == 'date new-old'
self.order(updated_at: :desc)
end
end
end
Once the function is called with a parameter, depending on the parameter that has been used, the list of products is ordered in a different way for the user to see.
I built a FactoryBot for the product model as well:
FactoryBot.define do
factory :product do
sequence(:name) { |n| "product_test#{n}" }
description { "Lorem ipsum dolor sit amet" }
price { rand(2000...10000) }
association :user, factory: :non_admin_user
rating { rand(1..5) }
trait :correct_availability do
availability { 1 }
end
trait :incorrect_availability do
availability { 3 }
end
#Adds a test image for product after being created
after(:create) do |product|
product.photos.attach(
io: File.open(Rails.root.join('test', 'fixtures', 'files', 'test.jpg')),
filename: 'test.jpg',
content_type: 'image/jpg'
)
end
factory :correct_product, traits: [:correct_availability]
factory :incorrect_product, traits: [:incorrect_availability]
end
end
Basically, we want to call the :correct_product to create a product thats accepted by the validations of the model.
For the specs:
describe ".order_list" do
let!(:first_product) { FactoryBot.create(:correct_product, name: "First Product" , rating: 1) }
let!(:second_product) { FactoryBot.create(:correct_product, name: "Second Product" , rating: 2) }
let!(:third_product) { FactoryBot.create(:correct_product, name: "Third Product" , rating: 3) }
it "orders products according to param" do
ordered_list = Product.order_list('rating 1-5')
expect(ordered_list.all).to eq[third_product, second_product, first_product]
end
end
So basically, my question is, how can I create an instance variable for each of the 3 mock products so I can name them here in the order I expect them to appear:
expect(ordered_list.all).to eq[third_product, second_product, first_product]
Or, even better, is there a way to create the instance variables with a loop and actually have the instance variable name to use them in the expect? This would free me from having to create 3 different variables on FactoryBot as I did.
I searched around the web and instance_variable_set is used in some cases:
Testing Rails with request specs, instance variables and custom primary keys
Simple instance_variable_set in RSpec does not work, but why not?
But it doesn´t seem to work for me.
Any idea on how could I make it work? Thanks!

How can I lessen the verbosity of my populate method?

I wrote a form object to populate an Order, Billing, and Shipping Address objects. The populate method looks pretty verbose. Since the form fields don't correspond to Address attributes directly, I'm forced to manually assign them. For example:
shipping_address.name = params[:shipping_name]
billing_address.name = params[:billing_name]
Here's the object. Note that I snipped most address fields and validations, and some other code, for brevity. But this should give you an idea. Take note of the populate method:
class OrderForm
attr_accessor :params
delegate :email, :bill_to_shipping_address, to: :order
delegate :name, :street, to: :shipping_address, prefix: :shipping
delegate :name, :street, to: :billing_address, prefix: :billing
validates :shipping_name, presence: true
validates :billing_name, presence: true, unless: -> { bill_to_shipping_address }
def initialize(item, params = nil, customer = nil)
#item, #params, #customer = item, params, customer
end
def submit
populate
# snip
end
def order
#order ||= #item.build_order do |order|
order.customer = #customer if #customer
end
end
def shipping_address
#shipping_address ||= order.build_shipping_address
end
def billing_address
#billing_address ||= order.build_billing_address
end
def populate
order.email = params[:email]
shipping_address.name = params[:shipping_name]
shipping_address.street = params[:shipping_street]
# Repeat for city, state, post, code, etc...
if order.bill_to_shipping_address?
billing_address.name = params[:shipping_name]
billing_address.street = params[:shipping_street]
# Repeat for city, state, post, code, etc...
else
billing_address.name = params[:billing_name]
billing_address.street = params[:billing_street]
# Repeat for city, state, post, code, etc...
end
end
end
Here's the controller code:
def new
#order_form = OrderForm.new(#item)
end
def create
#order_form = OrderForm.new(#item, params[:order], current_user)
if #order_form.submit
# handle payment
else
render 'new'
end
end
Noe I am not interested in accepts_nested_attributes_for, which presents several problems, hence why I wrote the form object.
def populate
order.email = params[:email]
shipping_params = %i[shipping_name shipping_street]
billing_params = order.bill_to_shipping_address? ?
shipping_params : %i[billing_name billing_street]
[[shipping_address, shipping_params], [billing_address, billing_params]]
.each{|a, p|
a.name, a.street = params.at(*p)
}
end
How about
class Order < ActiveRecord::Base
has_one :shipping_address, class_name: 'Address'
has_one :billing_address, class_name: 'Address'
accepts_nested_attributes_for :shipping_address, :billing_address
before_save :clone_shipping_address_into_billing_address, if: [check if billing address is blank]
Then when you set up the form, you can have fields_for the two Address objects, and side step the populate method entirely.
A possible fix would be to use a variable for retrieving those matching params, like so:
def populate
order.email = params[:email]
shipping_address.name = params[:shipping_name]
shipping_address.street = params[:shipping_street]
# etc...
#set a default state
shipping_or_billing = "shipping_"
#or use a ternary here...
shipping_or_billing = "billing_" if order.bill_to_shipping_address?
billing_address.name = params["shipping_or_billing" + "name"]
billing_address.street = params["shipping_or_billing" + "street"]
...
end
Your address classes should probably have a method that would set the values for all the address properties from a hash that it would receive as an argument.
That way your populate method would only check for order.bill_to_shipping_address? and them pass the correct dictionary to the method I'm suggesting.
That method on the other hand, would just assign the values from the hash to the correct properties, without the need for a conditional check.

shoulda factory girl error Couldn't find model without an ID

Good day, i get this error from
ActiveRecord::RecordNotFound:
Couldn't find User without an ID
my model
has_many :objects, class_name: 'OrderObject', dependent: :destroy
belongs_to :user
belongs_to :tariff
validates :client, :phone, :tariff_id, :days, :user_id, presence: true
spec
before do
user = FactoryGirl.create(:user)
FactoryGirl.create(:order, user_id: user.id)
end
context "validations" do
it { should validate_presence_of :client }
it { should validate_presence_of :phone }
it { should validate_presence_of :tariff_id }
it { should validate_presence_of :days }
end
it { should have_many(:objects) }
it { should belong_to(:tariff) }
it { should belong_to(:user) }
factory
factory :order do
client "MyString"
phone "MyString"
tariff_id 1
days 1
# advt_payed_day 1
# firm_payed_day 1
user_id 1
end
UPDATE 1
changed to
before(:all) do
user = FactoryGirl.create(:user )
puts user.id
order = FactoryGirl.create(:order, user_id: user.id )
puts order.id
end
output
Order
45
32
should have many objects
should belong to tariff
should belong to user
validations
should require client to be set (FAILED - 1)
should require phone to be set (FAILED - 2)
should require tariff_id to be set (FAILED - 3)
should require days to be set (FAILED - 4)
should require user_id to be set (FAILED - 5)
so order & user are created...
Update 2
as Rubyman suggested, i've changed couple of things:
in spec
before(:all) do
user = FactoryGirl.create(:user )
#puts user.id
order = FactoryGirl.create(:order, user_id: user.id )
puts order
puts order.user_id
end
in the factory
factory :order do
client "MyString"
phone "MyString"
tariff_id 1
days 1
# user_id 1
association :user, factory: :user
end
and output is:
Order
#<Order:0x00000005a866a0>
46
should have many objects
should belong to tariff
should belong to user
validations
should require client to be set (FAILED - 1)
should require phone to be set (FAILED - 2)
should require tariff_id to be set (FAILED - 3)
should require days to be set (FAILED - 4)
should require user_id to be set (FAILED - 5)
1) Order validations
Failure/Error: it { should validate_presence_of :client }
ActiveRecord::RecordNotFound:
Couldn't find User without an ID
# ./app/models/order.rb:35:in `user_is_not_admin?'
# ./spec/models/order_spec.rb:14:in `block (3 levels) in <top (required)>'
update 3
after reading advice from tdgs here are the changes:
in model no changes :
validates :client, :phone, :tariff_id, :days, :user_id, presence: true
in spec
describe Order do
before(:each) do
user = FactoryGirl.create(:user )
#puts user.id
order = FactoryGirl.create(:order, user_id: user.id )
puts order
puts order.user_id
puts order.tariff_id
puts order.phone
puts order.days
puts order.client
puts '*****'
user = User.find(order.user_id)
puts user.login
end
context "validations" do
it { should validate_presence_of :client }
it { should validate_presence_of :phone }
it { should validate_presence_of :tariff_id }
it { should validate_presence_of :days }
it { should validate_presence_of :user_id }
end
it { should have_many(:objects) }
it { should belong_to(:tariff) }
it { should belong_to(:user) }
end
output:
#<Order:0x00000006c10ce0>
161
101
MyString
1
MyString
*****
user__7
should require days to be set (FAILED - 1)
output for every should is valid as far as i see...
UPDATE N
should have written it in the beginning. i've run (hoped that it'll solve this issue) in console
bundle exec rake db:migrate
bundle exec rake db:migrate:reset db:test:prepare
First, your factory definition is not defining associations correctly. You should have something like this:
FactoryGirl.define do
factory :user do
sequence(:username) {|n| "username_#{n}"}
# more attributes here
end
factory :tariff do
# attributes
end
factory :order do
client "MyString"
phone "MyString"
tariff
user
days 1
end
end
Then your tests should be written like this:
context "validations" do
it { should validate_presence_of :client }
it { should validate_presence_of :phone }
it { should validate_presence_of :tariff_id }
it { should validate_presence_of :days }
end
it { should have_many(:objects) }
it { should belong_to(:tariff) }
it { should belong_to(:user) }
All the code you currently have in the before filter is not relevant right now. Also notice that using before(:all) might have some strange effects when running your tests, because they do not run inside a transaction. before(:each) on the other hand does.
try this
before {
#user = FactoryGirl.create(:user, :email => "test.com", :password => "test123", ... )
#order = FactoryGirl.create(:order, :user_id => #user.id )
}
Factory
require 'factory_girl'
FactoryGirl.define do
factory :order do
client "MyString"
...
...
end
end
Check how to create associations with factory girl
https://github.com/thoughtbot/factory_girl/blob/master/GETTING_STARTED.md
i moved away from shoulda and rewrote checks for validation. This way it works:
before(:each) do
#user = FactoryGirl.create(:user )
#order = FactoryGirl.create(:order, user_id: #user.id )
end
it 'absence of client isn\'t acceptable' do
temp = #order.client
#order.client = ''
#order.should_not be_valid
#order.client = temp
#order.should be_valid
end

Dynamically specifying returned values

I have a method within my main model, which should return specific values based on the set params:
def self.retrieve_profiles( params )
case params[:view]
when 'image_only'
fields = %w{ avatar }
when 'profile_minimal'
fields = %w{ avatar username age }
when 'profile_medium'
fields = %w{ avatar username age first_name last_name bio relationship_status seeking }
when 'profile_extended'
fields = %w{ avatar username age first_name last_name bio relationship_status seeking country city photos }
end
profiles = Profile.that_has_photos
profiles = profiles.where( :country_id => params['country'] ) unless params['country'].nil?
profiles = profiles.order( "RAND()" ).limit( params['count'] )
# works fine up to here (returns all fields)
profiles.each do |a|
fields.each do |field|
puts a.eval(field)
end
end
# above block makes it fail (private method `eval' called)
end
My question is: how do I return only the values specified by the fields hash ?
Use send instead of eval. This code should work fine.
profiles.each do |a|
fields.each do |field|
puts a.send(field)
end
end

Bulk insert using one model

I'm trying to create a form using textarea and a submit button that will allow users to do bulk insert. For example, the input would look like this:
0001;MR A
0002;MR B
The result would look like this:
mysql> select * from members;
+------+------+------+
| id | no | name |
+------+------+------+
| 1 | 0001 | MR A |
+------+------+------+
| 2 | 0002 | MR B |
+------+------+------+
I'm very new to Rails and I'm not sure on how to proceed with this one. Should I use attr_accessor? How do I handle failed validations in the form view? Is there any example? Thanks in advance.
Update
Based on MissingHandle's comment, I created a Scaffold and replace the Model's code with this:
class MemberBulk < ActiveRecord::Base
attr_accessor :member
def self.columns
#columsn ||= []
end
def self.column(name, sql_type = nil, default = nil, null = true)
columns << ActiveRecord::ConnectionAdapters::Column.new(name.to_s, default, sql_type.to_s, null)
end
column :data, :text
validates :data, :create_members, :presence => true
def create_members
rows = self.data.split("\r\n")
#member = Array.new
rows.each_with_index { |row, i|
rows[i] = row.strip
cols = row.split(";")
p = Member.new
p.no = cols[0]
p.name = cols[1]
if p.valid?
member << p
else
p.errors.map { |k, v| errors.add(:data, "\"#{row}\" #{v}") }
end
}
end
def create_or_update
member.each { |p|
p.save
}
end
end
I know the code is far from complete, but I need to know is this the correct way to do it?
class MemberBulk < ActiveRecord::Base
#Tells Rails this is not actually tied to a database table
# or is it self.abstract_class = true
# or #abstract_class = true
# ?
abstract_class = true
# members holds array of members to be saved
# submitted_text is the data submitted in the form for a bulk update
attr_accessor :members, :submitted_text
attr_accessible :submitted_text
before_validation :build_members_from_text
def build_members_from_text
self.members = []
submitted_text.each_line("\r\n") do |member_as_text|
member_as_array = member_as_text.split(";")
self.members << Member.new(:number => member_as_array[0], :name => member_as_array[1])
end
end
def valid?
self.members.all?{ |m| m.valid? }
end
def save
self.members.all?{ |m| m.save }
end
end
class Member < ActiveRecord::Base
validates :number, :presence => true, :numericality => true
validates :name, :presence => true
end
So, in this code, members is an array that is a collection of the individual Member objects. And my thinking is that as much as possible, you want to hand off work to the Member class, as it is the class that will actually be tied to a database table, and on which you can expect standard rails model behavior. In order to accomplish this, I override two methods common to all ActiveRecord models: save and valid. A MemberBulk will only be valid if all it's members are valid and it will only count as saved if all of it's members are saved. You should probably also override the errors method to return the errors of it's underlying members, possibly with an indication of which one it is in the submitted text.
In the end I had to change from using Abstract Class to Active Model (not sure why, but it stoppped working the moment I upgrade to Rails v3.1). Here's the working code:
class MemberBulk
include ActiveModel::Validations
include ActiveModel::Conversion
extend ActiveModel::Naming
attr_accessor :input, :data
validates :input, presence: true
def initialize(attributes = {})no
attributes.each do |name, value|
send("#{name}=", value) if respond_to?("#{name}=")
end
end
def persisted?
false
end
def save
unless self.valid?
return false
end
data = Array.new
# Check for spaces
input.strip.split("\r\n").each do |i|
if i.strip.empty?
errors.add(:input, "There shouldn't be any empty lines")
end
no, nama = i.strip.split(";")
if no.nil? or nama.nil?
errors.add(:input, "#{i} doesn't have no or name")
else
no.strip!
nama.strip!
if no.empty? or nama.empty?
errors.add(:input, "#{i} doesn't have no or name")
end
end
p = Member.new(no: no, nama: nama)
if p.valid?
data << p
else
p.errors.full_messages.each do |error|
errors.add(:input, "\"#{i}\": #{error}")
end
end
end # input.strip
if errors.empty?
if data.any?
begin
data.each do |d|
d.save
end
rescue Exception => e
raise ActiveRecord::Rollback
end
else
errors.add(:input, "No data to be processed")
return false
end
else
return false
end
end # def
end

Resources