How to make this code more Ruby-way? - ruby-on-rails

I am new to Ruby and Rails (switched from Python and Python frameworks). I'm writing a simple dashboard website which displays information about the S.M.A.R.T. state of hard disks. Here I wrote a helper to display a badge in a table cell near the relevant S.M.A.R.T attribute if it's value meets a condition. At first, the helper code was as simple as in Listing 1, but then I decided to draw a summary of all badges for the specific drive, in addition to the badges near individual S.M.A.R.T. attributes in the table. So at first I added a simple method like:
def smart_chk_device(attrs)
attrs.each { |item| smart_chk_attr(item) }
end
But this approach didn't work and it caused the entire array of attributes to be output to the resulting page. It only started to work when I made it as in Listing 2, but I believe there's something wrong there, and the same thing can be done in a more simple way. Please show me the right, Ruby-way of doing it.
Listing 1:
module HomeHelper
def smart_chk_attr(attr)
case attr[:id].to_i
when 1,197
content_tag(:span, "Warning", :class => "label label-warning") if attr[:raw].to_i > 0
when 5,7,10,196,198
content_tag(:span, "Critical", :class => "label label-important") if attr[:raw].to_i > 0
end
end
end
Listing 2 (works, but I don't like it):
module HomeHelper
def smart_chk_attr(attr)
case attr[:id].to_i
when 1,197
return content_tag(:span, "Warning", :class => "label label-warning") if attr[:raw].to_i > 0
when 5,7,10,196,198
return content_tag(:span, "Critical", :class => "label label-important") if attr[:raw].to_i > 0
else
return String.new
end
return String.new
end
def smart_chk_device(attrs)
output = ""
attrs.each { |item| output << smart_chk_attr(item) }
return output.html_safe
end
end
attrs is an Array of Hashes, where each Hash contains keys :id and :raw with the numeric code of a S.M.A.R.T attribute and its RAW value, both in Strings.
Also, RoR complaints if to remove the last "return String.new" in Listing 2. Why is it so? Doesn't the "case" block all the possible cases, so that control should never reach the end of the function?

I believe this would behave in the same way, and is much shorter:
module HomeHelper
def smart_chk_attr(attr)
return '' unless attr[:raw].to_i > 0
case attr[:id].to_i
when 1,197
content_tag(:span, "Warning", :class => "label label-warning")
when 5,7,10,196,198
content_tag(:span, "Critical", :class => "label label-important")
else ''
end
end
def smart_chk_device(attrs)
attrs.map { |item| smart_chk_attr(item) }.join.html_safe
end
end
Ruby methods return the value of the last expression, so get rid of the explicit returns all over that method. Also, DRY: Don't Repeat Yourself (the attr[:raw] check). In this case, I replaced those with a guard clause at the start of the method. Short-circuiting guard clauses are a matter of taste, but I like them and you'll see them in a lot of Ruby code.

Your method smart_chk_attr(attr) has an extra return at the end, it will never get run.
The each is an enumerator, when it finishes going through each item that you supplied it returns the the original object passed into it, not the modified stuff inside.
If you use collect you will get an array with your modified objects.
If you are wanting a string output you can use join to put them into a string. Join will also take an option for how to join your items.
def smart_chk_device(attrs)
attrs.collect{ |item| smart_chk_attr(item) }.join.html_safe
end

Related

Adding User Input to a hash? Ruby

I am trying to create some simple programs as trying to learn Ruby and then move on to rails, I am just playing about to try and get used to the flow of how different types of code work variables, loops etc.
I am trying to create a simple book system were I already have 3 books in my hash and then I want to list the books already in the library in the console and then I want to be able to add new books and then also loop through and display the new list to the console.
require 'rubygems'
class GetDetailsFromUser
books = {
Eagle_Eye: 1,
Eage_Eye1: 2,
Eagle_Eye2: 3
}
books.each do |i|
puts i
end
while true
add = gets.chomp
break if add.empty?
books << add
end
puts 'New list is below'
books.each do |i|
puts i
end
end
Were am I going wrong? I manage to print out the hash to the console, however, I get an error message
undefined method '<<' for {:Eagle_Eye=>1,...
Why is the method undefined? books << add? This should add a new book to the book hash table?
Add your second number with it. Here is a working example I wrote for you
Live Demo - VISIT THIS LINK
books = {
Eagle_Eye: 1,
Eage_Eye1: 2,
Eagle_Eye2: 3
}
books.each do |i|
puts i
end
while true
puts "What book would you like to add?"
add = gets.chomp
if add.empty? == true
puts "Error - You did not enter a title for a book"
break
else
books.max_by do |book,id|
#list_number = id
end
books[add.to_sym]=#list_number
break
end
end
puts 'New list is below'
books.each do |i|
puts i
end
refactored version of #ExecutiveCloser
books = {
Eagle_Eye: 1,
Eage_Eye1: 2,
Eagle_Eye2: 3
}
books.each do |i|
puts i
end
add = ""
while add.empty? do
puts "What book would you like to add?"
add = gets.chomp
books[add.to_sym] = books.size + 1 unless add.empty?
end
puts 'New list is below'
books.each do |i|
puts i
end
https://repl.it/CDK2/3
Ruby has a documentation site. E. g. Hash documentation clearly states, that there is no method << defined on the hash instance.
<< method is defined on Array.
Why would you expect “books << add should add a new book to the book hash table”?
There is another glitch within your code: you execute everything on class definition level, which makes absolutely no sense in this case.
Since it is unclear what do you want to achieve, I am unable to provide a ready-to-go-with solution.

Alternatives to nested conditionals Rails controller

I have a page that presents bits of information in a grid format. What shows up in each grid tile depends on:
Whether the user is logged in or not
What the user clicks on
Other factors
I'm using AJAX to send the controller what the user clicks on and grab fresh content for the tiles.
# Simplified pseudocode example
def get_tile_content
tile_objects = []
if current_user.present?
if params[:user_selected_content] == 'my stuff'
tile_objects = [... some model scope source here...]
elsif params[:user_selected_content] == 'new stuff'
tile_objects = [... some model scope source here...]
elsif params[:user_selected_content] == 'other stuff'
tile_objects = [... some model scope source here...]
end
else
tile_objects = [... some model scope source here...]
end
render :json => tile_objects.to_json || {}
end
Any ideas on how to approach this differently? I tried moving the complexity to models but I found it to be less readable and harder to figure out what is going on.
Looks like a decent case for a case statement (...see what I did there? ;P)
def get_tile_content
tile_objects = []
if current_user.present?
tile_objects = case params[:user_selected_content]
when 'my stuff' then Model.my_stuff
when 'new stuff' then Model.new_stuff
when 'other stuff' then Model.other_stuff
end
else
tile_objects = Model.public_stuff
end
render :json => tile_objects.to_json || {}
end
Sometimes case statements are necessary. Can't really say if that's the situation here since I can't see the larger design of your app, but this at least will clean it up a bit to fewer, easier to read lines, depending on your style preference.
You could wrap the case statement into its own method if you prefer, passing it the value of your parameter.
Another style point is that you don't typically use get_ at the beginning of ruby method names. It's assumed that .blah= is a setter and .blah is a getter, so .get_blah is redundant.

Problems querying JSON nested hash response in Ruby

Cannot seem to solve this problem:
I'm getting JSON nested hash responses from Lastfm and everything works fine when the response is structures as such:
{"topalbums" =>{"album" =>{"name =>"Friday Night in Dixie"}}}
However if the artist does not have a top album the response is structured this way and I get a NoMethodError undefined method '[]' for nil:NilClass.
{"topalbums" =>{"#text"=>"\n ", "artist"=>"Mark Chestnutt"}}
What I want to do is query the response so I do not keep getting this error.
Here is my method:
def get_albums
#albums = Array.new
#artistname.each do |name|
s = LastFM::Artist.get_top_albums(:artist => name, :limit => 1)
r = JSON.parse(s.to_json)['topalbums']['album']['name']
#albums.push(r)
end
end
which gives me exactly what I want if the artist has a top album, what I need to do is somehow add a condition to query the keys in the nested hash. However, I cannot seem to grasp how to do this as when I add this line of code to check key values:
s.each_key { |key, value| puts "#{key} is #{value}" }
the output I get is this:
topalbums is
so topalbums key does not have a value associated with it.
This is what I have tried so far:
def get_albums
#albums = Array.new
#artistname.each do |name|
s = LastFM::Artist.get_top_albums(:artist => name, :limit => 1)
if s.has_key?('album') #I know this won't work but how can I query this?
r = JSON.parse(s.to_json)['topalbums']['album']['name']
#albums.push r
else
#albums.push(name << "does not have a top album")
end
end
end
How can I fix this so I get 'Mark Chestnut does not have a top album' instead of the NoMethodError? Cheers
Use Hash#fetch default values, I would do as below:
No "album" key present
hash = {"topalbums" =>{"#text"=>"\n ", "artist"=>"Mark Chestnutt"}}
default_album = {"name" => "does not have a top album"}
hash["topalbums"].fetch("album", default_album)["name"]
#=> "does not have a top album"
"album" key present
hash = {"topalbums" =>{"#text"=>"\n ", "artist"=>"Mark Chestnutt", "album" => {"name" => "Foo"}}}
hash["topalbums"].fetch("album", default_album)["name"]
#=> "Foo"
So if the hash does not have an "album" key fetch defaults to default_album else it uses the key it find as in the second case

Can I use AR object as hash key or should I use object_id instead

Because of Ruby awesomeness it is possible to use any object as key
document = Document.find 1
o = Hash.new
o[1] = true
o[:coool] = 'it is'
o[document] = true
# an it works
o[document]
#=> true
but just because it is possible doesn't mean is good practice
However I have situation where in my controller I need to set something similar, so I can loop trough it in view
#controller
#users_with_things = Hash.new
Things.accessible_by(some_curent_user_logic).each do |thing|
#user_with_things[thing.user] ||= Array.new
#user_with_things[thing.user] << thing.id
end
#view
- #users_with_things.each do |user, thing_ids|
%input{type: :checkbox, name: "blank[user_#{user.id}]", value: 1, class: "select_groups", :'data-resource-ids' => "[#{thing_ids.join(',')}]", :'data-user-type' => user.type }
The reason why I want to do it this way is because I don't want to call from my view User.find_by_id (want to make it clean)
#controller
#users_with_things = Hash.new
Things.accessible_by(some_curent_user_logic).each do |thing|
#user_with_things[thing.user.id] ||= Array.new
#user_with_things[thing.user.id] << thing.id
end
#view
- #users_with_things.each do |user_id, thing_ids|
- user = User.find user_id
%input{type: :checkbox, name: "blank[user_#{user.id}]", value: 1, class: "select_groups", :'data-resource-ids' => "[#{thing_ids.join(',')}]", :'data-user-type' => user.type }
So my 1st question is: is it ok to use ActiveRecord object as Hash key in situation like this
I can imagine several scenarios where this may go wrong (sessions, when object changes in model and so on) however this is just for rendering in a view
Alternative !
so this is one way to do it, the other may be like this
#controller
#users_with_things = Hash.new
Things.accessible_by(some_curent_user_logic).each do |thing|
#user_with_things[thing.user.object_id] ||= Array.new
#user_with_things[thing.user.object_id] << thing.id
end
#view
- #users_with_things.each do |user_object_id, thing_ids|
- user = ObjectSpace._id2ref(user_object_id) #this will find user object from object_id
%input{type: :checkbox, name: "blank[user_#{user.id}]", value: 1, class: "select_groups", :'data-resource-ids' => "[#{thing_ids.join(',')}]"", :'data-user-type' => user.type }
...which is even more, hardcore. However it is way around if for some reason hash[ARobject] = :something would create big memory cluster for some reason
question 2 : is it good idea to do it this way ?
to be complete there is also another alternative and that is
# ...
#user_with_thing[ [thing.user.id, thing.user.type] ] << thing_id
# ...
so basically array object will be key
#user_with_thing[ [1, 'Admin'] ]
#=> [1,2,3]
I think to use a hash is a good way to organise in your situation. However, I would advise against using the user or to big an object as hash keys, simply because it renders your hash unreadable and because it is really only this sole object with it's object id that can be used as a key.
o = Object.new
h = { o => 'something' }
h[Object.new] #=> nil
In your situation, this may not be an issue, because you simply need to iterate it. But it may be a shot in the leg as soon as you want to do something else with that hash, or you have different instances of the same Active Record Data (which is very common in Rails applications, unless you are a really paying attention what gets loaded when). Besides that, I think it is good to stick by the widely used convention to use simple objects (strings, symbols) as hash keys to make your code readable and maintainable.
Maybe it would be best to keep a two-dimensional hash, like this:
#users_with_things = Things.accessible_by(some_curent_user_logic).inject({}) do |a, thing|
user_id = thing.user.id
a[user_id] ||= { :user => thing.user, :things => [] }
a[user_id][:thing] << thing
a
end
Then you can iterate over #users_with_things in your view like this:
#users_with_things.each do |user_id, values|
# values[:user] is the user, values[:things] the array of things

Ruby on Rails: Execute Logic Based on Selected Menu

I have a class that I use to contain select menu options for property types. It works fine. However, I need to be able to verify the selection and perform specific logic based on the selected option. This needs to happen in my Ruby code and in JavaScript.
Here is the class in question:
class PropertyTypes
def self.[](id)
##types[id]
end
def self.options_for_select
##for_select
end
private
##types = {
1 => "Residential",
2 => "Commercial",
3 => "Land",
4 => "Multi-Family",
5 => "Retail",
6 => "Shopping Center",
7 => "Industrial",
8 => "Self Storage",
9 => "Office",
10 => "Hospitality"
}
##for_select = ##types.each_pair.map{|id, display_name| [display_name, id]}
end
What is the best way to verify the selection? I need to perform specific logic and display user interface elements based on each type of property type.
Since I am storing the id, I would be verifying that the id is a particular property type. Something like:
PropertyTypes.isResidential?(id)
Then this method would look like this:
def self.isResidential?(id)
##types[id] == "Residential"
end
But now I am duplicating the string "Residential".
For JavaScript, I assume I would make an ajax call back to the model to keep the verification code DRY, but this seems like over kill.
Do I need to manually create a verification method for each property type or can I use define_method?
This seems so basic yet I am confused and burned out on this problem.
Thanks
===
Here's my solution:
class << self
##types.values.each do |v|
# need to remove any spaces or hashes from the found property type
v = v.downcase().gsub(/\W+/, '')
define_method "is_#{v}?", do |i|
type_name = ##types[i]
return false if type_name == nil #in case a bogus index is passed in
type_name = type_name.downcase().gsub(/\W+/, '')
type_name == v
end
end
end
It sounds like you can benefit from some Ruby meta-programming. Try googling "ruby method_missing". You can probably do something quick & dirty along the lines of:
class PropertyTypes
def method_missing(meth, *args, &block)
if meth.to_s =~ /^is_(.+)\?$/
##types[args.first] == $1
else
super
end
end
end
On the ruby side you could also use something like this to define dynamically these methods:
class << self
##types.values.each do |v|
define_method "is_#{v}?", do |i|
##types[i] == v
end
end
end

Resources