Rails: can I use two nested map? - ruby-on-rails

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}]

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 find by attribute in association array

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.

Get columns names with ActiveRecord

Is there a way to get the actual columns name with ActiveRecord?
When I call find_by_sql or select_all with a join, if there are columns with the same name, the first one get overridden:
select locations.*, s3_images.* from locations left join s3_images on s3_images.imageable_id = locations.id and s3_images.imageable_type = 'Location' limit 1
In the example above, I get the following:
#<Location id: 22, name: ...
>
Where id is that of the last s3_image. select_rows is the only thing that worked as expected:
Model.connection.select_rows("SELECT id,name FROM users") => [["1","amy"],["2","bob"],["3","cam"]]
I need to get the field names for the rows above.
This post gets close to what I want but looks outdated (fetch_fields doesn't seem to exist anymore How do you get the rows and the columns in the result of a query with ActiveRecord? )
The ActiveRecord join method creates multiple objects. I'm trying to achieve the same result "includes" would return but with a left join.
I am attempting to return a whole lot of results (and sometimes whole tables) this is why includes does not suit my needs.
Active Record provides a #column_names method that returns an array of column names.
Usage example: User.column_names
two options
Model.column_names
or
Model.columns.map(&:name)
Example
Model named Rabbit with columns name, age, on_facebook
Rabbit.column_names
Rabbit.columns.map(&:name)
returns
["id", "name", "age", "on_facebook", "created_at", "updated_at"]
This is just way active record's inspect method works: it only lists the column's from the model's table. The attributes are still there though
record.blah
will return the blah attribute, even if it is from another table. You can also use
record.attributes
to get a hash with all the attributes.
However, if you have multiple columns with the same name (e.g. both tables have an id column) then active record just mashes things together, ignoring the table name.You'll have to alias the column names to make them unique.
Okay I have been wanting to do something that's more efficient for a while.
Please note that for very few results, include works just fine. The code below works better when you have a lot of columns you'd like to join.
In order to make it easier to understand the code, I worked out an easy version first and expanded on it.
First method:
# takes a main array of ActiveRecord::Base objects
# converts it into a hash with the key being that object's id method call
# loop through the second array (arr)
# and call lamb (a lambda { |hash, itm| ) for each item in it. Gets called on the main
# hash and each itm in the second array
# i.e: You have Users who have multiple Pets
# You can call merge(User.all, Pet.all, lambda { |hash, pet| hash[pet.owner_id].pets << pet }
def merge(mainarray, arr, lamb)
hash = {}
mainarray.each do |i|
hash[i.id] = i.dup
end
arr.each do |i|
lamb.call(i, hash)
end
return hash.values
end
I then noticed that we can have "through" tables (nxm relationships)
merge_through! addresses this issue:
# this works for tables that have the equivalent of
# :through =>
# an example would be a location with keywords
# through locations_keywords
#
# the middletable should should return as id an array of the left and right ids
# the left table is the main table
# the lambda fn should store in the lefthash the value from the righthash
#
# if an array is passed instead of a lefthash or a righthash, they'll be conveniently converted
def merge_through!(lefthash, righthash, middletable, lamb)
if (lefthash.class == Array)
lhash = {}
lefthash.each do |i|
lhash[i.id] = i.dup
end
lefthash = lhash
end
if (righthash.class == Array)
rhash = {}
righthash.each do |i|
rhash[i.id] = i.dup
end
righthash = rhash
end
middletable.each do |i|
lamb.call(lefthash, righthash, i.id[0], i.id[1])
end
return lefthash
end
This is how I call it:
lambmerge = lambda do |lhash, rhash, lid, rid|
lhash[lid].keywords << rhash[rid]
end
Location.merge_through!(Location.all, Keyword.all, LocationsKeyword.all, lambmerge)
Now for the complete method (which makes use of merge_through)
# merges multiple arrays (or hashes) with the main array (or hash)
# each arr in the arrs is a hash, each must have
# a :value and a :proc
# the procs will be called on values and main hash
#
# :middletable will merge through the middle table if provided
# :value will contain the right table when :middletable is provided
#
def merge_multi!(mainarray, arrs)
hash = {}
if (mainarray.class == Hash)
hash = mainarray
elsif (mainarray.class == Array)
mainarray.each do |i|
hash[i.id] = i.dup
end
end
arrs.each do |h|
arr = h[:value]
proc = h[:proc]
if (h[:middletable])
middletable = h[:middletable]
merge_through!(hash, arr, middletable, proc)
else
arr.each do |i|
proc.call(i, hash)
end
end
end
return hash.values
end
Here's how I use my code:
def merge_multi_test()
merge_multi!(Location.all,
[
# each one location has many s3_images (one to many)
{ :value => S3Image.all,
:proc => lambda do |img, hash|
if (img.imageable_type == 'Location')
hash[img.imageable_id].s3_images << img
end
end
},
# each location has many LocationsKeywords. Keywords is the right table and LocationsKeyword is the middletable.
# (many to many)
{ :value => Keyword.all,
:middletable => LocationsKeyword.all,
:proc => lambda do |lhash, rhash, lid, rid|
lhash[lid].keywords << rhash[rid]
end
}
])
end
You can modify the code if you wish to lazy load attributes that are one to many (such as a City is to a Location) Basically, the code above won't work because you'll have to loop through the main hash and set the city from the second hash (There is no "city_id, location_id" table). You could reverse the City and Location to get all the locations in the city hash then extract back. I don't need that code yet so I skipped it =)

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

Resources