Rails find by attribute in association array - ruby-on-rails

I have two model A and B. A has_many B's. B has an attribute :number
What is the rails way (I could do some coding with each, but that's not the point) to find if an A has a B object with a given number ?
I've tried find but since it's an association, it gives me this error:
>> bs.find{|f| f.number == 8}
>> ActiveRecord::RecordNotFound: Couldn't find A without an ID
EDIT
To make more clear.
If I had to code this would be something like:
def is_number_in_use(number)?
self.bs.each do |b| #Consider bs as the has_many association between A and B
return true if b.numero == number
end
return false
end

Is that better?
a.bs.select{ |b| b.number == 8 }.any? #=> return true if a has one b o more with number == 8

find on your association is being over-ridden by ActiveRecord. I think what you want is Enumerable#select:
bs = B.all
bs_with_number_eq_8 = bs.select {|f| f.number == 8}
That won't generate a SQL query but will just iterate over the collection bs and filter them on number == 8

I don't understand what you want.
Can you put much code and I can have more information.
Also you can try this:
bs.find{|f| f.try(:number) == 8}

Try this,
def is_number_in_use(number)?
self.bs.collect(&:number).include?(number)
end
Update
self.bs.where(:number => some_number).present? #this will return true or false
and this will call only one sql query.

Related

Rails apply a condition within a sort_by looking for value

I'm working on sorting a list of employees by their title with the following:
TAGS = {
'Region Manager' => 1,
'Region Sales Manager' => 2,
'General Manager' => 3,
'Residential Sales Manager' => 4,
'Commercial Sales Manager' => 5,
'Other' => 6
}.freeze
def sorting_by_title(employees)
employees.sort_by do |x|
TAGS[x[:title]]
end
end
Works fine but...I also need to do an additional sort if an employee last name is Smith and needs to go before other individuals.
So I've tried doing something like:
return TAGS[x[:title]] unless x.last_name == "Smith"
Not working. It's erroring out on the show page with undefined method `each' for 2:Integer.
So my thought was I would build out another method to look for the last name.
def nepotism(employees)
employees.group_by do |emp|
case emp.last_name
when /Smith/ then :moocher
end
end
end
So I tried then referencing it like:
return TAGS[x[:title]] unless x.nepotism
return TAGS[x[:title]] unless x.moocher
Neither of those work. Nepotism ends up with undefined method `nepotism' for # and Moocher ends up with the same. Then I realized a simple query would work a bit better:
def nepotism
#nepotism = Employee.where(last_name: "Smith")
end
Is there a better way to sort_by a last_name if it matches Smith and THEN by the tags?
Here's a nice trick: in ruby you can compare arrays. And, consequently, use them as value in sort_by. They are compared element by element. If ary1[0] < ary2[0], then ary1 will be less than ary2, no matter the rest of the elements.
employees.sort_by do |x|
[
x.last_name == "Smith" ? 0 : 1, # all zeroes come before all ones
TAGS[x[:title]] # your main ordering parameter
]
end
This would work very well, if there were many Smiths and you needed to sort them by titles between themselves. If there's only one Smith, then #Björn's solution is simpler.
Combine them like this
employees.sort_by do |x|
x.last_name == "Smith" ? 0 : TAGS[x[:title]]
end
You can do it in the database as well (assuming Postgresql here)
def nepotism
tagstring = "array_position(ARRAY"+TAGS.keys.to_s.gsub(/\"/,"'")+", last_name)"
#nepotism = Employee.order("last_name = 'Smith' desc, " + tagstring)
end

What is the result of this query written in ruby in rails helper?

I am new in rails and I have started working on a project which has a model named 'Study', and here I am unable to know the result of this query written in rails helper.
def available_list_of(entities, exclude: nil)
case entities
when :studies then Study.where('id NOT IN (?)', exclude.nil? ? [-1] : exclude.pluck(:study_id) + [-1]).list
end
end
Its some pretty dense that code that can be expanded to:
def available_list_of(entities, exclude: nil)
# why use a case statement if there is only one option?
if entities == :studies
if exclude.nil?
Study.where('id NOT IN (?)', '-1')
else
Study.where('id NOT IN (?)', (exclude.pluck(:study_id) + [-1]))
end
end
end
What the actual purpose of the code is is beyond me though as it could simply be done with a scope:
class Study < ApplicationRecord
def self.excluding(excluded)
excluded.nil? ? self : self.where.not(id: excluded)
end
end
And also why would your typical auto incrementing id column ever contain -1?
When your expression works you can use the to_sql method, check the documentation here this method is from ActiveRecord::Relation
As an example:
Incident.where(reference: "PATAPAM").to_sql
=> "SELECT `incidents`.* FROM `incidents` WHERE `incidents`.`reference` = 'PATAPAM'"
I have myself found its solution.
It says that get all the records from Study model, where id is not equal to -1 if 'exclude' is 'nil' else get the 'study_id' from the exclude model and append -1 with study_id. And finally list function is a model function which will add name and id into the list

Rails: can I use two nested map?

def has_name? name
results = auths.map do |auth|
auth.role_groups.map do |role_group|
role_group.resources.any?{ |r| r.name == name}
end
end
results.any?
end
This is a method in User model
1 user has many auths
1 auth has many role_groups
1 role_group has many resources
I used two map there, but it does not return results I expect. This is the first time I two nested map, can I use it like this?
You can, but the result will have array of array and it isn't considered empty.
[[]].any?
=> true
#flat_map might help you here
def has_name? name
results = auths.flat_map do |auth|
auth.role_groups.map do |role_group|
role_group.resources.any?{ |r| r.name == name}
end
end
results.any?
end
Or you could change your solution altogether to more performant one with sql (without seeing your models, not sure it will work)
auths.joins(role_groups: :resources).where(resources: { name: name }).exists?
Firstly, you can add a direct relationship between auth and resources.
In the Auth model:
has_many: resources, through: role_groups
the has-many-through relationship can also be used for nested has-many relationships(like in your case). Check out the last example (document, section, paragraph relationships) in here: http://guides.rubyonrails.org/association_basics.html#the-has-many-through-association
Then you can do as follows:
def has_name? name
auths.includes(:resources).flat_map(&:resources).any? do |resource|
resource.name == name
end
end
Yes you can use nested maps to get the Cartesian product (simple list of all combinations) of two arrays:
a = [1,2,3]
b = [4,5,6]
l = a.map { |i|
b.map { |j|
{"a": i, "b": j}
}
}.flatten(1)
l result:
=> [{:a=>1, :b=>4}, {:a=>1, :b=>5}, {:a=>1, :b=>6}, {:a=>2, :b=>4}, {:a=>2, :b=>5}, {:a=>2, :b=>6}, {:a=>3, :b=>4}, {:a=>3, :b=>5}, {:a=>3, :b=>6}]

How to test records order in Rails?

I find very verbose and tedious to test if records coming from the database are correctly ordered.
I'm thinking using the array '==' method to compare two searches arrays. The array's elements and order must be the same so it seems a good fit. The issue is that if elements are missing the test will fail even though they are strictly ordered properly.
I wonder if there is a better way...
Rails 4
app/models/person.rb
default_scope { order(name: :asc) }
test/models/person.rb
test "people should be ordered by name" do
xavier = Person.create(name: 'xavier')
albert = Person.create(name: 'albert')
all = Person.all
assert_operator all.index(albert), :<, all.index(xavier)
end
Rails 3
app/models/person.rb
default_scope order('name ASC')
test/unit/person_test.rb
test "people should be ordered by name" do
xavier = Person.create name: 'xavier'
albert = Person.create name: 'albert'
assert Person.all.index(albert) < Person.all.index(xavier)
end
I haven't come across a built-in way to do this nicely but here's a way to check if an array of objects is sorted by a member:
class MyObject
attr_reader :a
def initialize(value)
#a = value
end
end
a = MyObject.new(2)
b = MyObject.new(3)
c = MyObject.new(4)
myobjects = [a, b, c]
class Array
def sorted_by?(method)
self.each_cons(2) do |a|
return false if a[0].send(method) > a[1].send(method)
end
true
end
end
p myobjects.sorted_by?(:a) #=> true
Then you can use it using something like:
test "people should be ordered by name by default" do
people = Person.all
assert people.sorted_by?(:age)
end
I came across what I was looking for when I asked this question. Using the each_cons method, it makes the test very neat:
assert Person.all.each_cons(2).all?{|i,j| i.name >= j.name}
I think having your record selection sorted will give you a more proper ordered result set, and in fact its always good to order your results
By that way I think you will not need the array == method
HTH
sameera

Mongoid random document

Lets say I have a Collection of users. Is there a way of using mongoid to find n random users in the collection where it does not return the same user twice? For now lets say the user collection looks like this:
class User
include Mongoid::Document
field :name
end
Simple huh?
Thanks
If you just want one document, and don't want to define a new criteria method, you could just do this:
random_model = Model.skip(rand(Model.count)).first
If you want to find a random model based on some criteria:
criteria = Model.scoped_whatever.where(conditions) # query example
random_model = criteria.skip(rand(criteria.count)).first
The best solution is going to depend on the expected size of the collection.
For tiny collections, just get all of them and .shuffle.slice!
For small sizes of n, you can get away with something like this:
result = (0..User.count-1).sort_by{rand}.slice(0, n).collect! do |i| User.skip(i).first end
For large sizes of n, I would recommend creating a "random" column to sort by. See here for details: http://cookbook.mongodb.org/patterns/random-attribute/ https://github.com/mongodb/cookbook/blob/master/content/patterns/random-attribute.txt
MongoDB 3.2 comes to the rescue with $sample (link to doc)
EDIT : The most recent of Mongoid has implemented $sample, so you can call YourCollection.all.sample(5)
Previous versions of mongoid
Mongoid doesn't support sample until Mongoid 6, so you have to run this aggregate query with the Mongo driver :
samples = User.collection.aggregate([ { '$sample': { size: 3 } } ])
# call samples.to_a if you want to get the objects in memory
What you can do with that
I believe the functionnality should make its way soon to Mongoid, but in the meantime
module Utility
module_function
def sample(model, count)
ids = model.collection.aggregate([
{ '$sample': { size: count } }, # Sample from the collection
{ '$project': { _id: 1} } # Keep only ID fields
]).to_a.map(&:values).flatten # Some Ruby magic
model.find(ids)
end
end
Utility.sample(User, 50)
If you really want simplicity you could use this instead:
class Mongoid::Criteria
def random(n = 1)
indexes = (0..self.count-1).sort_by{rand}.slice(0,n).collect!
if n == 1
return self.skip(indexes.first).first
else
return indexes.map{ |index| self.skip(index).first }
end
end
end
module Mongoid
module Finders
def random(n = 1)
criteria.random(n)
end
end
end
You just have to call User.random(5) and you'll get 5 random users.
It'll also work with filtering, so if you want only registered users you can do User.where(:registered => true).random(5).
This will take a while for large collections so I recommend using an alternate method where you would take a random division of the count (e.g.: 25 000 to 30 000) and randomize that range.
You can do this by
generate random offset which will further satisfy to pick the next n
elements (without exceeding the limit)
Assume count is 10, and the n is 5
to do this check the given n is less than the total count
if no set the offset to 0, and go to step 8
if yes, subtract the n from the total count, and you will get a number 5
Use this to find a random number, the number definitely will be from 0 to 5 (Assume 2)
Use the random number 2 as offset
now you can take the random 5 users by simply passing this offset and the n (5) as a limit.
now you get users from 3 to 7
code
>> cnt = User.count
=> 10
>> n = 5
=> 5
>> offset = 0
=> 0
>> if n<cnt
>> offset = rand(cnt-n)
>> end
>> 2
>> User.skip(offset).limit(n)
and you can put this in a method
def get_random_users(n)
offset = 0
cnt = User.count
if n < cnt
offset = rand(cnt-n)
end
User.skip(offset).limit(n)
end
and call it like
rand_users = get_random_users(5)
hope this helps
Since I want to keep a criteria, I do:
scope :random, ->{
random_field_for_ordering = fields.keys.sample
random_direction_to_order = %w(asc desc).sample
order_by([[random_field_for_ordering, random_direction_to_order]])
}
Just encountered such a problem. Tried
Model.all.sample
and it works for me
The approach from #moox is really interesting but I doubt that monkeypatching the whole Mongoid is a good idea here. So my approach is just to write a concern Randomizable that can included in each model you use this feature. This goes to app/models/concerns/randomizeable.rb:
module Randomizable
extend ActiveSupport::Concern
module ClassMethods
def random(n = 1)
indexes = (0..count - 1).sort_by { rand }.slice(0, n).collect!
return skip(indexes.first).first if n == 1
indexes.map { |index| skip(index).first }
end
end
end
Then your User model would look like this:
class User
include Mongoid::Document
include Randomizable
field :name
end
And the tests....
require 'spec_helper'
class RandomizableCollection
include Mongoid::Document
include Randomizable
field :name
end
describe RandomizableCollection do
before do
RandomizableCollection.create name: 'Hans Bratwurst'
RandomizableCollection.create name: 'Werner Salami'
RandomizableCollection.create name: 'Susi Wienerli'
end
it 'returns a random document' do
srand(2)
expect(RandomizableCollection.random(1).name).to eq 'Werner Salami'
end
it 'returns an array of random documents' do
srand(1)
expect(RandomizableCollection.random(2).map &:name).to eq ['Susi Wienerli', 'Hans Bratwurst']
end
end
I think it is better to focus on randomizing the returned result set so I tried:
Model.all.to_a.shuffle
Hope this helps.

Resources