I'm building a custom profile completeness tool in Rails 3.1. Using active_admin, the administrator wants to be able to pick model attributes and assign a score value to them. I create a table with 'name' and 'score' in it. The name being the column name to apply the score to.
I need to compare the values from the name column in the scoring table when the model gets updated. Psuedocode:
ProfileScore.find(:all, :select => "name,score").inject { |p,score|
#this is where im stuck, 'self' == updated model hash
p[:name] == self.attribute.updated
score += p[:score]
}
Of course other approaches are welcome! I looked at completeness-fu but it is way out of date.
score = ProfileScore.select("name, score").inject(0) { |memo, pscore|
# the result is a list of ProfileScore objects, not of pairs.
# if p[:name] == self.attribute.updated # don't you want to take into account
# also old, but present, attributes??
if self.attributes[p[:name]].present?
memo += p[:score]
end
memo
}
or also
present_attributes = self.attributes.reject { |k,v| v.blank? }.keys
score = ProfileScore.where(:name => present_attributes).sum("score")
To get the total score, you can use:
total_score = ProfileScore.where(:name => self.attribute.updated).sum(:score)
Related
How to compare objects with the same parameter in ruby? How to define elsif part?
def compare
array_of_items = #items.map(&:object_id)
if array_of_items.uniq.size == array_of_items.size #array has only uniq vlaues - it's not possible to duplicate object - good!
return
elsif
#the comparision of objects with the same object_id by other param (i.e. date_of_lease param). The part I can not formulate
else
errors.add('It is not possible to purchase many times one item with the same values')
end
end
You can use Enumerable#group_by, e.g.
elsif #items.group_by(&:date_of_lease).count == array_of_items.size
As far as I understand, I guess you want to compare two objects with same object_id.
same_objects = #items.select { | element |
if array_of_items.count(element.object_id) > 1 do
# Duplicate object
end
}
I don't know how about other Ruby implementations, but in MRI Object#object_id returns unique (integer representation of the object in memory) value for every object. If you try to redefine it, you will get the warning:
class Object
def object_id
'a'
end
end
#=> warning: redefining `object_id' may cause serious problems
:object_id
First of all, since this is tagged as rails, isn't this the type of thing you can solve with a built in validation?
validates_uniqueness_of :date_of_lease, scope: :object_id
I don't know know about your implementation, but if you used the primary key of your database you might not even need that scope.
Otherwise, assuming you have overriden ruby object_id so two objects can have the same ids (¿?) I can only think of something complex like:
def compare
duplicate_items = #items.group_by(&:object_id).select { |k,v| v.size > 1}
if duplicate_items.keys.empty?
return
elsif duplicate_items.select{|k,v| v.group_by(&:date_of_lease).count != v.count}.empty?
# There are no duplicate object ids that also have duplicate
# dates of lease between themselves
else
errors.add('It is not possible to purchase many times one item with the same values')
end
end
Check that you have to handle the case where there are different object ids with the same date of lease in the same items array that has duplicates, that should be valid. For example : Item id 1, date 12, Item id 1, date 13, item id 2, date 12 Should be valid.
I have a database of different food menus that I am trying to search through. In general everything works fine, but I think that there must be a cleverer way in writing the code compared to what I am doing now.
Every menu has a set of boolean attributes describing the kind of kitchen (e.g. cuisine_thai, cuisine_italian, etc.). In my view I have a dropdown allowing the user to select the type of food he wants and then I am passing the param on and save it in my search-object.
#search.cuisine_type = params[:cuisine_type]
I then continue to check for the different kitchen types and see if there is a match.
#Filter for Thai cuisine
if(#search.cuisine_type == "cuisine_thai")
#menus = #menus.select{ |menu| menu.cuisine_thai == true}
end
#Filter for French cuisine
if(#search.cuisine_type == "cuisine_italian")
#menus = #menus.select{ |menu| menu.cuisine_italian == true}
end
#Filter for Peruvian cuisine
if(#search.cuisine_type == "cuisine_peruvian")
#menus = #menus.select{ |menu| menu.cuisine_peruvian == true}
end
Eventhough the way I do it works, there must be a better way to do this. I feel that the value stored in #search.cuisine_type could just determine the attribute I check on #menus.
Any help on this is greatly appreciated. Thanks!
Yes, your intuition is correct!
I'll assume #menus is an array are ActiveRecord Menu objects, and that the cuisine_* attributes correspond to database columns. In this case you can use ActiveRecord's attributes method.
Every ActiveRecord object has an attributes property. The docs (Rails 4.2.1) say:
attributes()
Returns a hash of all the attributes with their names as keys and the values of the attributes as values.
To verify this is the case, if you peek at the attributes of a given menu, you should see a hash containing:
{
"cuisine_italian" => true,
"cuisine_thai" => false,
# etc.
}
Also, as a minor point of code readability, these two statements are effectively the same:
#menus = #menus.select { ... }
#menus.select! { ... }
Therefore, your search can be rewritten as:
if #search.cuisine_type.present?
#menus.select! { |m| m.attributes[#search.cuisine_type] }
end
Wouldn't it be better if you had column "cuisine" in database and have it set to thai, italian and so on?
Then you'd only check if certain food matches array of kitchens selected by the user.
I have an application that dynamically creates a drop-down menu based on certain values in the database. Often the drop-down values are just in the order they come up but I would like to put them in a certain order.
An example of my value system:
Newbie = 0
Amateur = 1
Skilled = 2
Pro = 3
GrandMaster = 4
How would I take the data above and use it to sort an array full of those values (Newbie etc). I've thought about creating a hash of the values but even then I still am not sure how to apply that to the sort method.
Any help would be appreciated.
You can sort this array just by using the usual sorting the sorting won't be done by name it will be done by value. and if these are not integer objects and are some user defined class then sorting based on a particular attribute can be achieved very efficiently by
lst.sort_by &:first
where first is the attribute of the object.
Sort has by value:
hash = {:Newbie=>0, :Amateur=>1, :Skilled=>2, :Pro=>3}
> hash.sort { hash{a} <=> hash{b} }
=> [[:Newbie, 0], [:Amateur, 1], [:Skilled, 2], [:Pro, 3]]
Or use Ruby Hash#sort_by method:
hash.sort_by { |k,v| v }
Suppose you have a Level model that has a sort_id identifying the displayed order and a name holding the displayed name. I recommend using default_scope to set the default order for that model because it is likely that you always want to sort Level records this way:
class Level < ActiveRecord::Base
#### attributes
# id (integer)
# name (string)
# sort_id (integer)
default_scope order('sort_id ASC')
# rest of model...
end
Then, the only thing you have to do in your view to display a picklist is
<%= f.select("level", Level.pluck(:name)) %>
An alternate to #padde. I prefer to avoid default scopes.
class Level < ActiveRecord::Base
#### attributes
# id (integer)
# name (string)
# value (integer)
end
In the view
<%= f.select("level", Level.order(:value).map{|l| [l.name, l.value] } %>
Due to my poorly explained question the others trying to answer my question didn't really get a chance but using their help I did manage to figure out my problem.
ex_array = ["GrandMaster", "Newbie", "Pro", "Skilled", "Amateur"]
value_sys = {:Newbie=>0, :Amateur=>1, :Skilled=>2, :Pro=>3, :GrandMaster=>4}
ex_array.sort { |a,b| value_sys[a.to_sym] <=> value_sys[b.to_sym]
=> ["Newbie", "Amateur", "Skilled","Pro", "GrandMaster"]
Thanks for the help guys. Much appreciated.
Parse your value system for further use:
values = <<EOF
Newbie = 0
Amateur = 1
Skilled = 2
Pro = 3
GrandMaster = 4
EOF
value_map = Hash[values.split("\n").map{|v| v.split(/\s*=\s*/)}.map{|v| [v[0], v[1].to_i]}]
#=> {"Newbie"=>0, "Amateur"=>1, "Skilled"=>2, "Pro"=>3, "GrandMaster"=>4}
Assign the array values a weight according to value_map to transform the array into a new one, sort according to the weight, and then transform the new array back.
# here I created a sample array
array = value_map.keys.shuffle
#=> ["Newbie", "Pro", "Skilled", "Amateur", "GrandMaster"]
# transform and sort
sorted = array.map{|v| [v, value_map[v] || 0xFFFF]}.sort_by{|v| v[1]}.map{|v| v[0]}
#=> ["Newbie", "Amateur", "Skilled", "Pro", "GrandMaster"]
Or you can bypass the transform step and just use the sort_by method:
sorted = array.sort_by{|v| value_map[v] || 0xFFFF}
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 =)
I have the following two models:
Project, has_many ProjectParticipants
ProjectParticipants, belongs_to Project
How can I request to determine given these 5 ProjectParticipants, do they belong to a Project?
Also, it should be strictly those 5, not more or less.
Any thoughts on how to elegantly solve for this type of count?
Assuming participants contain the 5 participants you want to check.
participants.all? {|o| o.project }
This will return true of all participants have a project, otherwise false.
To return the project that was found you can do:
And to see if all participants have the same project:
first_participant = participants.shift
participants.all? {|o| o.project == first_participant.project} unless first_participant.nil?
The good thing about this method is that it short circuits if one of the participant's doesn't have the same project(more efficient).
Edit:
To return the project that they all share, you can do:
first_participant = participants.shift
project_shared = participants.all? {|o| o.project == first_participant.project} and first_particpant.project unless first_participant.nil?
project_shared will be set to the project that they are all share, otherwise it will be to nil/false.
So you can then do:
if project_shared
# do work
else
# they dont share a project!
end
You can compare the properties of the ProjectParticipant records in a group:
participants.length == 5 and participants.all? { |p| p.project_id == project.id }
This validates that your array of participants contains five entries and that all of them have the same project_id assigned. Comparing p.project == project will have the side-effect of loading the same Project five times.
To check if they simply belong to a project, you can do this:
participants.length == 5 and participants.all? { |p| p.project_id? }
That project may be deleted and the reference could be invalid, so you may need to resort to actually fetching it:
participants.length == 5 and participants.all? { |p| p.project }
You can also use the group_by method to see if there's only one project involved:
grouped = participants.group_by(&:project)
!grouped[nil] and grouped.length == 1 and grouped.first[1].length == 5
The group_by method will organize the given array into a hash where the key is specified as a parameter and the value is a list of objects matching. It can be handy for situations like this.