Problem sorting an array - ruby-on-rails

I'm mixing 2 arrays and want to sort them by their created_at attribute:
#current_user_statuses = current_user.statuses
#friends_statuses = current_user.friends.collect { |f| f.statuses }
#statuses = #current_user_statuses + #friends_statuses
#statuses.flatten!.sort!{ |a,b| b.created_at <=> a.created_at }
The #current_user_statuses and #friends_statuses each sort correctly, but combined they sort incorrectly, with the #friends_statuses always showing up on top sorted by their created_at attribute and the #current_user_statuses on the bottom sorted by their created_at attribute.
This is the view:
<% #statuses.each do |d| %>
<%= d.content %>
<% end %>

Try:
(current_user.statuses + current_user.friends.collect(&:statuses)) \
.flatten.compact.sort_by(&:created_at)

You can not daisy chain the flatten! method like that. flatten! returns nil if no changes were made to the array. When you sort nil nothing will happen.
You need to separate them:
#statuses.flatten!
#statuses.sort! { ... }

Here's how I'd do it:
Set up the classes:
class User
class Status
attr_reader :statuses, :created_at
def initialize(stats)
#statuses = stats
#created_at = Time.now
end
end
attr_reader :statuses, :friends
def initialize(stats=[], friends=[])
#statuses = Status.new(stats)
#friends = friends
end
end
Define some instances, with some time gaps just for fun:
friend2 = User.new(%w[yellow 2])
sleep 1
friend1 = User.new(%w[orange 1])
sleep 2
current_user = User.new(%w[green 1], [friend1, friend2])
Here's how I'd do it differently; Get the statuses in created_at order:
statuses = [
current_user.statuses,
current_user.friends.collect(&:statuses)
].flatten.sort_by(&:created_at)
Which looks like:
require 'pp'
pp statuses
# >> [#<User::Status:0x0000010086bd60
# >> #created_at=2011-07-02 10:49:49 -0700,
# >> #statuses=["yellow", "2"]>,
# >> #<User::Status:0x0000010086bc48
# >> #created_at=2011-07-02 10:49:50 -0700,
# >> #statuses=["orange", "1"]>,
# >> #<User::Status:0x0000010086bb30
# >> #created_at=2011-07-02 10:49:52 -0700,
# >> #statuses=["green", "1"]>]
I'm just building a temporary containing array to hold the current_user's status, plus the status of all the friends, then flattening it.
The (&:statuses) and (&:created_at) parameters are Rails short-hand for the statuses method of the instance, or created_at method of the instance.

#statuses = (#current_user_statuses + #friends_statuses).sort_by(&:created_at)

I know there are several solutions posted for your question. But all of these solutions can kill your system when the number of statuses grow in size. For this dataset, you have to perform the sorting and pagination in the database layer and NOT in the Ruby layer
Approach 1: Simple and concise
Status.find_all_by_user_id([id, friend_ids].compact, :order => :created_at)
Approach 2: Long and efficient
class User
def all_statuses
#all_statuses ||=Status.all( :joins => "JOIN (
SELECT friend_id AS user_id
FROM friendships
WHERE user_id = #{self.id}
) AS friends ON statuses.user_id = friends.user_id OR
statuses.user_id = {self.id}",
:order => :created_at
)
end
end
Now you can get the sorted statuses in single query:
user.all_statuses
PPS: If this is my code I would further optimize the SQL. Refer to this answer for some more details.

Related

Why group calculation fields do not show up in query result?

I have query like this:
query = Link.select('url, max(created_at) as created_at, count(*) as url_count').group(:url).order('url_count desc, created_at asc')
Sample results of query.results.first:
2.2.0 :002 > query.first
=> #<Link id: nil, url: "http://1", created_at: "2015-03-10 16:43:54">
Why there is no url_count here, even though I know it is.
2.2.0 :003 > query.first.url_count
=> 17
The count is there all along but the model to_s method does not know about it.
The to_s method which is used when your console logs the result from query.first is defined somewhere in activerecord and it uses the attributes defined for the model in the database. Since your attribute is only defined for this particular instance of Link not for the model Link.
I found this quite interesting. Below is a description of how the message displayed in your console is constructed. It starts out in the gem active_attr with these 3 methods displayed below:
def inspect
attribute_descriptions = attributes.sort.map { |key, value| "#{key}: #{value.inspect}" }.join(", ")
separator = " " unless attribute_descriptions.empty?
"#<#{self.class.name}#{separator}#{attribute_descriptions}>"
end
# ...
def attributes
attributes_map { |name| send name }
end
# ...
def attributes_map
Hash[ self.class.attribute_names.map { |name| [name, yield(name)] } ]
end
the method attributes_names is defined in the gem activerecord
def attribute_names
#attribute_names ||= if !abstract_class? && table_exists?
column_names
else
[]
end
end
# ...
def column_names
#column_names ||= #connection.columns(#table_name).collect { |c| c.name }
end
And that is why your count does not show up.
If you really want it to show up in your console you could override the inspect method and add it there.

Rails: How to initialize an object with the attributes in strings?

Probably been working on this too long, sloppy design, or both. My issue is I have a model I wish to initialize. The object has like 52 attributes, but I'm only setting a certain ~25 depending on which object I've just scanned. When I scan an object I get the columns and match them up with a hash_map I've created.
Example Hash Map
This just matches the scanned text to their respective attribute name.
hash_map = {"Pizza."=>"pizza_pie","PastaBowl"=>"pasta_bowl","tacos"=>"hard_shell_taco","IceCream"=>"ice_cream","PopTarts"=>"pop_tart"}
What I want to do
menu = RestaurantMenu.new(pizza_pie => var1, pasta_bowl => var2, ...)
My only problem is in my code at the moment I have this...
t.rows.each do |r|
for i in 0..r.length-1
#hash_map[t.combined_columns[i]] => r.[i]
puts "#{hash_map["#{t.combined_columns[i]}"]} => #{r[i]}"
end
end
the puts line displays what I want, but unsure how to get that in my app properly.
Here is several ways to fix this:
hash_map = {"Pizza."=>"pizza_pie","PastaBowl"=>"pasta_bowl","tacos"=>"hard_shell_taco","IceCream"=>"ice_cream","PopTarts"=>"pop_tart"}
attributes.each do |attribute, element|
message.send((attribute + '=').to_sym, hash_map[element])
end
or like this:
class Example
attr_reader :Pizza, :PastaBowl #...
def initialize args
args.each do |k, v|
instance_variable_set("##{k}", v) unless v.nil?
end
end
end
for more details click here
I ended up doing the following method:
attributes = Hash[]
attributes["restaurant"] = tmp_basic_info.name
attributes["menu_item"] = tmp_basic_info.item_name
t.rows.each do |r|
for i in 0..r.length-1
attributes["other"] = t.other_information
attributes[hash_map[t.combined_columns[i]] = r[i]
end
row = ImportMenuItem.new(attributes)
row.save
end

Using :includes with variables and further queries

Here's a query:
#projects = #user.projects.includes(:company => :workers)
In my view, I want to order a list of these projects by the tags that belong to the company.
So, projects belonging to the company's urgent tag come first, then elevated, then all others.
I think one way to do this is to define #companies, then define:
#urgent = #companies.tagged_with('urgent')
#elevated = #companies.tagged_with('elevated')
#others = #companies.tagged_with('urgent', 'elevated', :exclude => true)
# view:
#urgent.each do |u| ... end
#elevated.each do |e| ... end
#others.each do |o| ... end
1. How do I get #companies in the first place?
How can you collect all the companies together when the first query is the one I showed at the top? #companies = #projects.collect{|p| p.company} produces a non-searchable array.
2. Is there a more elegant way of displaying the projects than doing three loops?
I dont think you can do this without using 2 queries.
#projects = #user.projects.includes(:company => :workers)
#companies = Company.find(#projects.collect(&:id))
As for rendering the tags, you could do:
ordered_tags = ['urgent', 'elevated']
ordered_tags.each do |tag|
#companies.tagged_with(tag).each do |company|
my_render_logic
end
end
#companies.tagged_with(*ordered_tags, :exclude => true).each do |company|
my_render_logic
end

Removing “duplicate objects” with same attributes using Array.map

As you can see in the current code below, I am finding the duplicate based on the attribute recordable_id. What I need to do is find the duplicate based on four matching attributes: user_id, recordable_type, hero_type, recordable_id. How must I modify the code?
heroes = User.heroes
for hero in heroes
hero_statuses = hero.hero_statuses
seen = []
hero_statuses.sort! {|a,b| a.created_at <=> b.created_at } # sort by created_at
hero_statuses.each do |hero_status|
if seen.map(&:recordable_id).include? hero_status.recordable_id # check if the id has been seen already
hero_status.revoke
else
seen << hero_status # if not, add it to the seen array
end
end
end
Try this:
HeroStatus.all(:group => "user_id, recordable_type, hero_type, recordable_id",
:having => "count(*) > 1").each do |status|
status.revoke
end
Edit 2
To revoke the all the latest duplicate entries do the following:
HeroStatus.all(:joins => "(
SELECT user_id, recordable_type, hero_type,
recordable_id, MIN(created_at) AS created_at
FROM hero_statuses
GROUP BY user_id, recordable_type, hero_type, recordable_id
HAVING COUNT(*) > 1
) AS A ON A.user_id = hero_statuses.user_id AND
A.recordable_type = hero_statuses.recordable_type AND
A.hero_type = hero_statuses.hero_type AND
A.recordable_id = hero_statuses.recordable_id AND
A.created_at < hero_statuses.created_
").each do |status|
status.revoke
end
Using straight Ruby (not the SQL server):
heroes = User.heroes
for hero in heroes
hero_statuses = hero.hero_statuses
seen = {}
hero_statuses.sort_by!(&:created_at)
hero_statuses.each do |status|
key = [status.user_id, status.recordable_type, status.hero_type, status.recordable_id]
if seen.has_key?(key)
status.revoke
else
seen[key] = status # if not, add it to the seen array
end
end
remaining = seen.values
end
For lookups, always use Hash (or Set, but here I thought it would be nice to keep the statuses that have been kept)
Note: I used sort_by!, but that's new to 1.9.2, so use sort_by (or require "backports")

get a list of objects with roles applied in be9 acl9

I think in something like this:
def self.obj_list(opts = {:include => [] , :exclude => []})
# Returns an array with all objects with roles applied
# +:exclude+:: (array,string) optional object type to exclude from list
# +:include+:: (array,string) optional object type to include in list
# Example:
# Role.obj_list(:include => ["Device", "User"])
# Role.obj_list(:exclude => ["User"])
inc = opts[:include].to_a
exc = opts[:exclude].to_a
objs = []
if inc.empty?
self.all.each do |r|
unless r.authorizable_type.nil?
objs << r.authorizable_type.constantize.find(r.authorizable_id) unless exc.include?(r.authorizable_type)
end
end
else
self.all.each do |r|
unless r.authorizable_type.nil?
objs << r.authorizable_type.constantize.find(r.authorizable_id) if inc.include?(r.authorizable_type)
end
end
end
objs
end
You can use where clauses to do the include/exclude stuff in SQL:
( inc.empty?
? where.not( :authorizable_type => exc )
: where( :authorizable_type => inc )
).map(&:authorizable)
By using authorizable you will get Rails's own handling of polymorphic associations, which will ensure that only actual objects are returned, so there's no need to check for nil
I don't know, maybe you want to link the object and subject?
If is this, here are a tutorial for that: https://github.com/be9/acl9/wiki/tutorial:-linking-object-and-subject-with-hmt

Resources