Access attributes in ActiveRecord::Associations::CollectionProxy - ruby-on-rails

I am trying to write a custom function that will throw an error if the amount of associated objects are >=4
I am wondering how i can access the keys/values in the contained hash and run a validation on it
if i do this
animal = FactoryGirl.create(:animal, images_count: 4)
a = animal.animal_images
ap(a)
I get this returned
#<ActiveRecord::Associations::CollectionProxy [
#<AnimalImage id: 520, animal_id: 158, image: "yp2.jpg", created_at: "2014-10-15 13:45:11", updated_at: "2014-10-15 13:45:11">,
#<AnimalImage id: 521, animal_id: 158, image: "yp2.jpg", created_at: "2014-10-15 13:45:11", updated_at: "2014-10-15 13:45:11">,
#<AnimalImage id: 522, animal_id: 158, image: "yp2.jpg", created_at: "2014-10-15 13:45:11", updated_at: "2014-10-15 13:45:11">,
#<AnimalImage id: 523, animal_id: 158, image: "yp2.jpg", created_at: "2014-10-15 13:45:11", updated_at: "2014-10-15 13:45:11">
]
So i thought of using .map
animal = FactoryGirl.create(:animal, images_count: 4)
a = animal.animal_images
map = a.each.map { |i| i.image }
if map.length >= 4
ap('MORE THAN 4 IMAGES')
end
"MORE THAN 4 IMAGES"
So that iterates through the CollectionProxy. However how can i get this formatted into a correct rspec test and perform the logic in a custom validation function.
I thought my test would look like this
it 'should display an error message when too many images are uploaded' do
animal = FactoryGirl.create(:animal, images_count: 4)
animal.max_num_of_images
expect(animal.errors[:base]).to include("Max of 3 images allowed")
end
and just to get the pass for now (add error message) with no logic i have
class AnimalImage < ActiveRecord::Base
belongs_to :animal
validate :max_num_of_images, :if => "image?"
def max_num_of_images
errors.add(:base, "Max of 3 images allowed")
end
end
but it seems as if the test doesnt get past the first line
ActiveRecord::RecordInvalid:
Validation failed: Max of 3 images allowed
the above is thrown in the console
This is my factory
FactoryGirl.define do
factory :animal, class: Animal do
ignore do
images_count 0
end
after(:create) do |animal, evaluator|
create_list(:animal_image, evaluator.images_count, animal: animal)
end
end
end
FactoryGirl.define do
factory :animal_image do
image { File.open("#{Rails.root}/spec/fixtures/yp2.jpg") }
end
end
i'm probably going about this in the most backwards way possible, does anyone have any suggestions please
thanks

I am trying to write a custom function that will throw an error if the amount of associated objects are >=4
You are overcomplicating things. If you just want to count the number of records in a collection then you can simply do animal.animal_images.size. So your model will look like this:
class Animal < ActiveRecord::Base
has_many :animal_images
validate :max_num_of_images
def max_num_of_images
errors.add(:base, "Max of 3 images allowed") if self.animal_images.size >= 4
end
end

Related

Rspec - archives array to match tested array

I'm having this class method on my Post model for getting archives
def self.archives
Post.unscoped.select("YEAR(created_at) AS year, MONTHNAME(created_at) AS month, COUNT(id) AS total")
.group("year, month, MONTH(created_at)")
.order("year DESC, MONTH(created_at) DESC")
end
This is the test I have wrote for my method
context '.archives' do
first = FactoryGirl.create(:post, published_at: Time.zone.now)
second = FactoryGirl.create(:post, published_at: 1.month.ago)
it 'returns articles archived' do
archives = Post.archives()
expect(
[{
year: first.published_at.strftime("%Y"),
month: first.published_at.strftime("%B"),
published: 1
},
{
year: second.published_at.strftime("%Y"),
month: second.published_at.strftime("%B"),
published: 1
}]
).to match_array(archives)
end
end
However I get the following error
expected collection contained: [#<Post id: nil>, #<Post id: nil>]
actual collection contained: [{:year=>"2017", :month=>"October", :published=>1}, {:year=>"2017", :month=>"September", :total=>1}]
the missing elements were: [#<Post id: nil>, #<Post id: nil>]
the extra elements were: [{:year=>"2017", :month=>"October", :total=>1}, {:year=>"2017", :month=>"September", :total=>1}]
So although I have created 2 factories, the archives array is empty. What am I doing wrong?
Rspec standard is to use the let syntax for defining variables within a context or describe block. The test should look something like this:
describe '.archives' do
let!(:first) { FactoryGirl.create(:post, published_at: Time.zone.now) }
let!(:second) { FactoryGirl.create(:post, published_at: 1.month.ago) }
it 'returns year, month, and total for articles archived' do
actual_attributes = Post.archives.map { |post| [post.year, post.month, post.total] }
expected_total = 1 # I don't know why the query is returning 1 for total, but including this for completeness
expected_attributes = [first, second].map { |post| [post.created_at.year, post.created_at.strftime("%B"), expected_total] }
expect(actual_attributes).to match_array(expected_attributes)
end
end
The issue here is that you are comparing records pulled with only a few attributes (the result of your SQL query) with fully-formed records (created by your test). This test pulls the applicable attributes from both groups and compares them.
Actual array is not empty, it's an array of two Post instances with ids unset (because Select in .archives method doesn't contain id field).
You could compare expected hashes not with archives, but with smth like that:
actual = Post.archives().map do |post|
{ year: post["year"].to_s, month: post["month"], published: post["total"] }
end
expected = [{
year: first.published_at.strftime("%Y").to_s,
month: first.published_at.strftime("%B"),
published: 1
},
{
year: second.published_at.strftime("%Y").to_s,
month: second.published_at.strftime("%B"),
published: 1
}]
expect(actual).to match_array(expected)

Ruby Array#sort_by on array of ActiveRecord objects seems slow

I'm writing a controller index method that returns a sorted array of ActiveRecord Contact objects. I need to be able to sort the objects by attributes or by the output of an instance method. For example, I need to be able to sort by contact.email as well as contact.photos_uploaded, which is an instance method that returns the number of photos a contact has.
I can't use ActiveRecord's native order or reorder method because that only works with attributes that are columns in the database. I know from reading that normally array#sort_by is much faster than array#sort for complex objects.
My question is, how can I improve the performance of this block of code in my controller method? The code currently
contacts = company.contacts.order(last_name: :asc)
if params[:order].present? && params[:order_by].present? && (Contact::READ_ONLY_METHOD.include?(params[:order_by].to_sym) || Contact::ATTRIBUTES.include?(params[:order_by].to_sym))
contacts = contacts.sort_by do |contact|
if params[:order_by] == 'engagement'
contact.engagement.to_i
else
contact.method(params[:order_by].to_sym).call
end
end
contacts.reverse! if params[:order] == 'desc'
end
The root problem here (I think) is that I'm calling sort_by on contacts, which is an ActiveRecord::Relation that could have several hundred contacts in it. Ultimately I paginate the results before returning them to the client, however they need to be sorted before they can be paginated. When I run the block of code above with 200 contacts, it takes an average of 900ms to execute, which could be a problem in a production environment if a user has thousands of contacts.
Here's my Contact model showing some relevant methods. The reason I have a special if clause for engagement is because that method returns a string that needs to be turned into an integer for sorting. I'll probably refactor that before I commit any of this to return an integer. Generally all the methods I might sort on return an integer representing the number of associated objects (e.g. number of photos, stories, etc that a contact has). There are many others, so for brevity I'm just showing a few.
class Contact < ActiveRecord::Base
has_many :invites
has_many :responses, through: :invites
has_many :photos
has_many :requests
belongs_to :company
ATTRIBUTES = self.attribute_names.map(&:to_sym)
READ_ONLY_METHOD = [:engagement, :stories_requested, :stories_submitted, :stories_published]
def engagement
invites = self.invites.present? ? self.invites.count : 1
responses = self.responses.present? ? self.responses.count : 0
engagement = ((responses.to_f / invites).round(2) * 100).to_i.to_s + '%'
end
def stories_requested
self.invites.count
end
def stories_submitted
self.responses.count
end
def stories_published
self.responses.where(published: true).count
end
end
When I run a query to get a bunch of contacts and then serialize it to get the values for all these methods, it only takes ~80ms for 200 contacts. The vast majority of the slowdown seems to be happening in the sort_by block.
The output of the controller method should look like this after I iterate over contacts to build a custom data structure, using this line of code:
#contacts = Hash[contacts.map { |contact| [contact.id, ContactSerializer.new(contact)] }]
I've already benchmarked that last line of code so I know that it's not a major source of slowdown. More on that here.
{
contacts: {
79: {
id: 79,
first_name: "Foo",
last_name: "Bar",
email: "t#t.co",
engagement: "0%",
company_id: 94,
created_at: " 9:41AM Jan 30, 2016",
updated_at: "10:57AM Feb 23, 2016",
published_response_count: 0,
groups: {
test: true,
test23: false,
Test222: false,
Last: false
},
stories_requested: 1,
stories_submitted: 0,
stories_published: 0,
amplify_requested: 1,
amplify_completed: 1,
photos_uploaded: 0,
invites: [
{
id: 112,
email: "t#t.co",
status: "Requested",
created_at: "Jan 30, 2016, 8:48 PM",
date_submitted: null,
response: null
}
],
responses: [ ],
promotions: [
{
id: 26,
company_id: 94,
key: "e5cb3bc80b58c29df8a61231d0",
updated_at: "Feb 11, 2016, 2:45 PM",
read: null,
social_media_posts: [ ]
}
]
}
}
}
if params[:order_by] == 'stories_submitted'
contact_ids = company.contact_ids
# count all invites that have the relevant contact ids
invites=Invite.where(contact_id:contact_ids).group('contact_id').count
invites_contact_ids = invites.map(&:first)
# Add contacts with 0 invites
contact_ids.each{|c| invites.push([c, 0]) unless invites_contact_ids.include?(c)}
# Sort all invites by id (add .reverse to the end of this for sort DESC)
contact_id_counts=invites.sort_by{|r| r.last}.map(&:first)
# The [0, 10] limits you to the lowest 10 results
contacts=Contact.where(id: contact_id_counts[0, 10])
contacts.sort_by!{|c| contact_id_counts.index(c.id)}
end

NoMethodError: undefined method `<<' for nil:NilClass

I have a task that I did not build but is no longer working. i am new to ruby on rails and have not found a solution for this problem yet.
when i run the task i Get:
this is the task:
desc "Archive Loads/Trucks that have expired."
task :expire_old_posts => :environment do
expired_loads = []
loads = Load.where("pickup < NOW()")
loads.each {|load| expired_loads << load.attributes }
ArchivedLoad.create( expired_loads )
loads.delete_all
end
little Debugging thru console
Confirms there are 120530 loads to transfer:
loads = Load.where("pickup < NOW()").count
2014-07-23 12:55:56 DEBUG -- (149.8ms) SELECT COUNT(*) FROM "loads" WHERE (pickup < NOW())
=> 120530
create the empty array:
expired_loads = []
=> []
run the where command
loads = Load.where("pickup < NOW()")
lots and lots of records... i limited it to two for testing
1.9.3-p547 :006 > loads = Load.where("pickup < NOW()").limit(2)
2014-07-23 13:02:07 DEBUG -- Load Load (74.2ms) SELECT "loads".* FROM "loads" WHERE (pickup < NOW()) LIMIT 2
=> [#<Load id: 18398947, user_id: 11074, origin: #<RGeo::Geographic::SphericalPointImpl:0x595f056 "POINT (-80.48237609863281 37.772544860839844)">, dest: #<RGeo::Geographic::SphericalPointImpl:0x595ec64 "POINT (-78.30302429199219 40.295894622802734)">, length: 48, comments: "~PostEverywhere_20140721140916~", ltl: false, rate: nil, delivery: "2014-07-22 17:00:00", pickup: "2014-07-21 17:00:00", weight: 48, equipment_id: 8, covered: false, created_at: "2014-07-21 19:16:16", updated_at: "2014-07-21 19:16:16", owner: nil, deleted: false, origin_city: "ronceverte", origin_state: "wv", dest_city: "martinsburg", dest_state: "pa">, #<Load id: 18398948, user_id: 11074, origin: #<RGeo::Geographic::SphericalPointImpl:0x553cd2a "POINT (-81.035400390625 37.384891510009766)">, dest: #<RGeo::Geographic::SphericalPointImpl:0x553c9d8 "POINT (-79.80570983886719 40.317527770996094)">, length: 48, comments: "~PostEverywhere_20140721140916~", ltl: false, rate: nil, delivery: "2014-07-22 17:00:00", pickup: "2014-07-21 17:00:00", weight: 48, equipment_id: 8, covered: false, created_at: "2014-07-21 19:16:16", updated_at: "2014-07-21 19:16:16", owner: nil, deleted: false, origin_city: "princeton", origin_state: "wv", dest_city: "greenock", dest_state: "pa">]
Really not sure what the << is but i assume this is just distributing the attributes so that they can be saved in the other table
loads.each {|load| expired_loads << load.attributes }
Errors here with
**NoMethodError: undefined method `<<' for nil:NilClass**
Next line is: Saves them to archived_loads table and deletes from loads table
ArchivedLoad.create( expired_loads )
loads.delete_all
Why not use #map instead? expired_loads = loads.map(&:attributes) or better yet just create a method in Load to archive them e.g.
class Load
scope :expired_loads, -> {where("pickup < NOW()")}
def self.archive_now
ArchiveLoad.create(expired_loads.map(&:attributes))
expired_loads.delete_all
end
end
Then you can call Loads.archive_now I am unsure of any of your structure this is just a suggestion as to a more concise implementation.
It also might be prudent to post the create method of your ArchivedLoad in case you have utilized the << method inside of there as well. If you have not implemented some kind of a custom create method I think you need a iteration instead such as
expired_loads.map(&:attributes).each{|load| ArchiveLoad.create(load)}
Also << is similar to push it adds an object to the end of an Array

fabricated models not the same as on disk

I'm probably misunderstanding something here.
I have a model Secondant that I create with Fabrication in my model rspec.
main_user = Fabricate :user, email: TESTEMAIL
sec = Fabricate :secondant, email: SECEMAIL, user_id: main_user.id
sec_user = Fabricate :user, email: SECEMAIL
ActionMailer::Base.deliveries = []
debugger
At this point when I look at the value of secondant_id in the sec model, the attribute is empty (it get's filled in a after_create callback). When i retrieve the model just created from the database that attribute is filled. Why are those two not in sync?
27 main_user = Fabricate :user, email: TESTEMAIL
28 sec = Fabricate :secondant, email: SECEMAIL, user_id: main_user.id
29 sec_user = Fabricate :user, email: SECEMAIL
30 ActionMailer::Base.deliveries = []
31 debugger
=> 32 sec.destroy
33 end
34
35 it 'should have a secondant_id assigned' do
36 sec.secondant_id.should_not be_nil
(rdb:1) e sec
#<Secondant id: 519, user_id: 1095, email: "secondant#hotmail.com", secondant_id: nil, created_at: "2013-10-10 13:13:29", updated_at: "2013-10-10 13:13:29", reported: false>
(rdb:1) e Secondant.where(id: sec.id).first
#<Secondant id: 519, user_id: 1095, email: "secondant#hotmail.com", secondant_id: 1096, created_at: "2013-10-10 13:13:29", updated_at: "2013-10-10 13:13:29", reported: false>
My after_create callback:
def find_user
user = User.where(email: self.email).first
if user
# create the link to the user
self.secondant_id = user.id
self.save
# see if this is the second one
if Secondant.where('user_id = ? and secondant_id is not null', user_id).count == 2
user.do_somthing
end
end
return
end
EDIT
There is a similar callback in the user class, which is firing in this case (thanks Peter)
def find_secondant
Secondant.where(email: email).find_each do |sec|
sec.secondant_id = id
sec.save
end
end
At the time you create sec, the user with the identical email has not been created, so your after_save callback should not be setting secondant_id.
I can only assume that your find_user method is getting invoked as a result of the User creation or the where operation you are executing in the debugger, resulting in the secondant_id field being set at that time. It won't be reflected in sec unless/until you do a reload, as the Ruby object created by where is distinct from the sec Ruby object.

getting attributes from a hash/difference between '.first' and '[i]'

I have the following class methods for goal
def evals
self.evaluations.order("eval_number").group_by(&:student_id)
end
def evals_for(student, i)
#evals = []
self.evals.values.each do |eval|
#evals << eval.keep_if { |e| e.student_id == student.id }
end
#evals = #evals.reject { |array| array.empty? }.first
#evals[i]
end
in the view, i'm calling the second method like this:
<% #student.student_group.eval_count.times do |i| %>
<td><%= goal.evals_for(#student, i) %></td>
<% end %>
which returns
#<Evaluation:x>, #<Evaluation:y>, #<Evaluation:z>
if i change the last line of the class method to call #evals[i].inspect, i can see the data inside each hash, like so:
#<Evaluation id: 1949, score: 3, created_at: "2013-08-28 09:44:32", updated_at: "2013-08-28 09:44:32", student_id: 32, goal_id: 63, eval_number: 29>
I want to get the score, but when I call #evals[i].score on the last line in the class method, i get an error - undefined method 'score' for nil:NilClass
I know I can't call class methods on hashes, but is there a way to pull out just that data? As a sub-question, I'm a bit confused about the difference between .first and [i] as calling #evals.first.score returns "3" - only I can't use first as I need to be able to access each instance of evaluation in turn.
This is not a simple Hash:
#<Evaluation id: 1949, score: 3, created_at: "2013-08-28 09:44:32", updated_at: "2013-08-28 09:44:32", student_id: 32, goal_id: 63, eval_number: 29>
It's an instance of Evaluation class so you should be able to call score on it. I think your issue is due to the fact that you are trying to call score on nil
You'll probably want to make sure that you have an instance object before calling the method:
#evals[i].score if #evals[i]

Resources