Looping an array to collect in Ruby on Rails - ruby-on-rails

Using Rails 3.2. I have the following:
FRUITS = %w(
apple
orange
)
FRUITS.each do |fruit|
define_method "#{fruit}" do
stalls.collect(&:fruit).join(' ')
end
end
The expected result is:
def apple
stalls.collect(&:apple).join(' ')
end
def orange
stalls.collect(&:orange).join(' ')
end
I've got a problem returning the fruit in .collect(&:fruit). What should I change? Thanks.

Use full block form (not the #to_proc shortcut)
FRUITS.each do |fruit|
define_method "#{fruit}" do
stalls.collect{|st| st.send(fruit.to_sym)}.join(' ')
end
end

You are using the symbol :fruit in your call to collect, so the generated methods will be like:
def orange
stalls.collect(&:fruit).join(' ')
end
You need to create the symbol from the fruit string (using String#to_sym) like this:
FRUITS.each do |fruit|
define_method "#{fruit}" do
stalls.collect(&fruit.to_sym).join(' ')
end
end

Related

Neat way to get and set keys of json column in Rails

I have a model/table with a json column in it as follows
t.json :options, default: {}
The column can contain many keys within it, something like this
options = {"details" : {key1: "Value1", key2: "Value2"}}
I want to set and get these values easily. So i have made getters and setters for the same.
def key1
options['details']&.[]('key1')
end
def key1=(value)
options['details'] ||= {}
options['details']['key1'] ||=0
options['details']['key1'] += value
end
But this just adds lines to my code, and it does not scale when more details are added. Can you please suggest a clean and neat way of doing this?
Use dynamic method creation:
options['details'].default_proc = ->(_,_) {{}}
ALLOWED_KEYS = %i[key1 key2 key3]
ALLOWED_KEYS.each do |key|
define_method key do
options['details'][key] if options['details'].key?(key)
end
define_method "#{key}=" do |value|
(options['details'][key] ||= 0) += value
end
end
You can just pass the key as a parameter as well right?
def get_key key=:key1
options['details']&.[](key)
end
def set_key= value, key=:key1
options['details'] ||= {}
options['details'][key] ||=0
options['details'][key] += value
end
Simple & Short
Depending on re-usability you can choose different options. The short option is to simply define the methods using a loop in combination with #define_method.
class SomeModel < ApplicationRecord
option_accessors = ['key1', 'key2']
option_accessors.map(&:to_s).each do |accessor_name|
# ^ in case you provide symbols in option_accessors
# this can be left out if know this is not the case
define_method accessor_name do
options.dig('details', accessor_name)
end
define_method "#{accessor_name}=" do |value|
details = options['details'] ||= {}
details[accessor_name] ||= 0
details[accessor_name] += value
end
end
end
Writing a Module
Alternatively you could write a module that provide the above as helpers. A simple module could look something like this:
# app/model_helpers/option_details_attribute_accessors.rb
module OptionDetailsAttributeAccessors
def option_details_attr_reader(*accessors)
accessors.map(&:to_s).each do |accessor|
define_method accessor do
options.dig('details', accessor)
end
end
end
def option_details_attr_writer(*accessors)
accessors.map(&:to_s).each do |accessor|
define_method "#{accessor}=" do |value|
details = options['details'] ||= {}
details[accessor] ||= 0
details[accessor] += value
end
end
end
def option_details_attr_accessor(*accessors)
option_details_attr_reader(*accessors)
option_details_attr_writer(*accessors)
end
end
Now you can simply extend your class with these helpers and easily add readers/writers.
class SomeModel < ApplicationRecord
extend OptionDetailsAttributeAccessors
option_details_attr_accessor :key1, :key2
end
If anything is unclear simply ask away in the comments.

How to create an array of methods in rails

def id_attachment_require_upload?
!object.id_attachment?
end
...
def work_attachment_require_upload?
!object.work_attachment?
end
I want to make it like below.
array = %w(id address work)
array.each do |a|
def #{a}_attachment_require_upload?
!object.#{a}_attachment?
end
end
Is there any way for me to create a array of methods automatically in rails to save me from the redundant work.
array = %w(id address work)
array.each do |a|
define_method "#{a}_attachment_require_upload?" do
!object.public_send("#{a}_attachment?")
end
end
Arup's answer looks like it's the way to go but I'm not sure if object.#{a}_attachment? will work. If it does, then I learned something new today. You can also use public_send.
array = %w[id address work]
array.each do |a|
define_method "#{a}_attachment_require_upload?" do
!object.public_send("#{a}_attachment?")
end
end

Ruby/Rails: Prepend, append code to all methods

I wrote a small benchmarking Class for testing my code doing development. At the moment I have to add the Class to the beginning and end of every method. Is it posible to prepend, append on the fly, so that I don't have to clutter my code?
class ApplicationController
before_filter :init_perf
after_filter :write_perf_results_to_log!
def init_perf
#perf ||= Perf.new
end
def write_perf_results_to_log!
#perf.results
end
end
class Products < ApplicationsController
def foo
#perf.log(__methond__.to_s)
caculation = 5 *4
#perf.write!
end
def bar
#perf.log(__methond__.to_s)
caculation = 1 / 5
#perf.write!
end
end
This is the Perf class. It is located in the services folder.
class Perf
def initialize
#results = []
end
def log(note)
#start = Time.now
#note = note
end
def write!
if #results.find {|h| h[:note] == #note } # Update :sec method exists in results
#results.select { |h| h["note"] == #note; h[":sec"] = (Time.now - #start).round(3) }
else # Add new Hash to results
#results << { :note => #note, :sec => (Time.now - #start).round(3) }
end
end
def results
content = "
PERFORMANCE STATISTICS!
"
#results.each do |r|
content += r[:note] + " " + r[:sec].to_s + "
"
end
content += "
"
Rails.logger.info content
end
end
In general computing terms what you want to do is called code instrumentation. There are several ways to accomplish this, however here's one (crude) example using some metaprogramming:
First define a new method that we will use for injecting our instrumentation code:
class ApplicationController
def self.instrument_methods(*methods)
methods.each { |m|
# Rename original method
self.send(:alias_method, "#{m}_orig", m)
# Redefine old method with instrumentation code added
define_method m do
puts "Perf log #{m}"
self.send "#{m}_orig"
puts "Perf write"
end
}
end
end
How to use it:
class Product < ApplicationController
def foo
puts "Foo"
end
def bar
puts "Bar"
end
# This has to be called last, once the original methods are defined
instrument_methods :foo, :bar
end
Then:
p = Product.new
p.foo
p.bar
Will output:
Perf log foo
Foo
Perf write
Perf log bar
Bar
Perf write
Here are some other ways to instrument ruby code and measure performance:
http://ruby-prof.rubyforge.org/
http://www.igvita.com/2009/06/13/profiling-ruby-with-googles-perftools/
There is better solution.
class ApplicationController
def self.inherited(klass)
def klass.method_added(name)
return if #_not_new
#_not_new = true
original = "original #{name}"
alias_method original, name
define_method(name) do |*args, &block|
puts "==> called #{name} with args: #{args.inspect}"
result = send original, *args, &block
puts "<== result is #{result}"
result
end
#_not_new = false
end
end
end
class Product < ApplicationController
def meth(a1, a2)
a1 + a2
end
end
product = Product.new
puts product.meth(2,3)
And the result:
==> called meth with args: [2, 3]
<== result is 5
5
The source & explanation are here: http://pragprog.com/screencasts/v-dtrubyom/the-ruby-object-model-and-metaprogramming. I recommend to spend not a big money to get this course.
I'm the author of aspector gem. Thanks to dimuch for mentioning it.
I've come up with a solution using aspector. Here are the high level steps:
Create an aspect as a subclass of Aspector::Base
Inside the aspect, define advices (before/after/around are the primary types of advices)
Apply the aspect on target class (or module/object)
The full code can be found in this gist. Please feel free to let me know if you have questions or the solution doesn't do what you intend to.
class PerfAspect < Aspector::Base
around options[:action_methods] do |proxy|
#perf ||= Perf.new
proxy.call
#perf.results
end
around options[:other_methods], :method_arg => true do |method, proxy, *args, &block|
#perf.log(method)
result = proxy.call *args, &block
#perf.write!
result
end
end
action_methods = [:action]
other_methods = Products.instance_methods(false) - action_methods
PerfAspect.apply(Products, :action_methods => action_methods, :other_methods => other_methods)
Guess aspector gem can help. It's not well documented but has useful examples.

Decimals and commas when entering a number into a Ruby on Rails form

What's the best Ruby/Rails way to allow users to use decimals or commas when entering a number into a form? In other words, I would like the user be able to enter 2,000.99 and not get 2.00 in my database.
Is there a best practice for this?
Does gsub work with floats or bigintegers? Or does rails automatically cut the number off at the , when entering floats or ints into a form? I tried using self.price.gsub(",", "") but get "undefined method `gsub' for 8:Fixnum" where 8 is whatever number I entered in the form.
I had a similar problem trying to use localized content inside forms. Localizing output is relatively simple using ActionView::Helpers::NumberHelper built-in methods, but parsing localized input it is not supported by ActiveRecord.
This is my solution, please, tell me if I'm doing anything wrong. It seems to me too simple to be the right solution. Thanks! :)
First of all, let's add a method to String.
class String
def to_delocalized_decimal
delimiter = I18n::t('number.format.delimiter')
separator = I18n::t('number.format.separator')
self.gsub(/[#{delimiter}#{separator}]/, delimiter => '', separator => '.')
end
end
Then let's add a class method to ActiveRecord::Base
class ActiveRecord::Base
def self.attr_localized(*fields)
fields.each do |field|
define_method("#{field}=") do |value|
self[field] = value.is_a?(String) ? value.to_delocalized_decimal : value
end
end
end
end
Finally, let's declare what fields should have an input localized.
class Article < ActiveRecord::Base
attr_localized :price
end
Now, in your form you can enter "1.936,27" and ActiveRecord will not raise errors on invalid number, because it becomes 1936.27.
Here's some code I copied from Greg Brown (author of Ruby Best Practices) a few years back. In your model, you identify which items are "humanized".
class LineItem < ActiveRecord::Base
humanized_integer_accessor :quantity
humanized_money_accessor :price
end
In your view templates, you need to reference the humanized fields:
= form_for #line_item do |f|
Price:
= f.text_field :price_humanized
This is driven by the following:
class ActiveRecord::Base
def self.humanized_integer_accessor(*fields)
fields.each do |f|
define_method("#{f}_humanized") do
val = read_attribute(f)
val ? val.to_i.with_commas : nil
end
define_method("#{f}_humanized=") do |e|
write_attribute(f,e.to_s.delete(","))
end
end
end
def self.humanized_float_accessor(*fields)
fields.each do |f|
define_method("#{f}_humanized") do
val = read_attribute(f)
val ? val.to_f.with_commas : nil
end
define_method("#{f}_humanized=") do |e|
write_attribute(f,e.to_s.delete(","))
end
end
end
def self.humanized_money_accessor(*fields)
fields.each do |f|
define_method("#{f}_humanized") do
val = read_attribute(f)
val ? ("$" + val.to_f.with_commas) : nil
end
define_method("#{f}_humanized=") do |e|
write_attribute(f,e.to_s.delete(",$"))
end
end
end
end
You can try stripping out the commas before_validation or before_save
Oops, you want to do that on the text field before it gets converted. You can use a virtual attribute:
def price=(price)
price = price.gsub(",", "")
self[:price] = price # or perhaps price.to_f
end
Take a look at the i18n_alchemy gem for date & number parsing and localization.
I18nAlchemy aims to handle date, time and number parsing, based on current I18n locale format. The main idea is to have ORMs, such as ActiveRecord for now, to automatically accept dates/numbers given in the current locale format, and return these values localized as well.
I have written following code in my project. This solved all of my problems.
config/initializers/decimal_with_comma.rb
# frozen_string_literal: true
module ActiveRecord
module Type
class Decimal
private
alias_method :cast_value_without_comma_separator, :cast_value
def cast_value(value)
value = value.gsub(',', '') if value.is_a?(::String)
cast_value_without_comma_separator(value)
end
end
class Float
private
alias_method :cast_value_without_comma_separator, :cast_value
def cast_value(value)
value = value.gsub(',', '') if value.is_a?(::String)
cast_value_without_comma_separator(value)
end
end
class Integer
private
alias_method :cast_value_without_comma_separator, :cast_value
def cast_value(value)
value = value.gsub(',', '') if value.is_a?(::String)
cast_value_without_comma_separator(value)
end
end
end
end
module ActiveModel
module Validations
class NumericalityValidator
protected
def parse_raw_value_as_a_number(raw_value)
raw_value = raw_value.gsub(',', '') if raw_value.is_a?(::String)
Kernel.Float(raw_value) if raw_value !~ /\A0[xX]/
end
end
end
end
I was unable to implement the earlier def price=(price) virtual attribute suggestion because the method seems to call itself recursively.
I ended up removing the comma from the attributes hash, since as you suspect ActiveRecord seems to truncate input with commas that gets slotted into DECIMAL fields.
In my model:
before_validation :remove_comma
def remove_comma
#attributes["current_balance"].gsub!(',', '') # current_balance here corresponds to the text field input in the form view
logger.debug "WAS COMMA REMOVED? ==> #{self.current_balance}"
end
Here's something simple that makes sure that number input is read correctly. The output will still be with a point instead of a comma. That's not beautiful, but at least not critical in some cases.
It requires one method call in the controller where you want to enable the comma delimiter. Maybe not perfect in terms of MVC but pretty simple, e.g.:
class ProductsController < ApplicationController
def create
# correct the comma separation:
allow_comma(params[:product][:gross_price])
#product = Product.new(params[:product])
if #product.save
redirect_to #product, :notice => 'Product was successfully created.'
else
render :action => "new"
end
end
end
The idea is to modify the parameter string, e.g.:
class ApplicationController < ActionController::Base
def allow_comma(number_string)
number_string.sub!(".", "").sub!(",", ".")
end
end
You can try this:
def price=(val)
val = val.gsub(',', '')
super
end

HTML escaped in Rails 3

I have a method call in my view like this
<%= Navigation.with(params) do |menu|
if current_user && current_user.can_verify?
menu.item("Listings", manage_listings_path())
menu.item("Listing changes", needing_change_approval_manage_listings_path())
menu.item("Flagged Items", flagged_manage_listings_path())
menu.item("Transfers", manage_listing_transfers_path())
menu.item("Reviews", manage_listing_reviews_path())
end
if current_user && current_user.admin?
menu.item("Log", manage_verifications_path())
menu.item("Indexer Compensations", manage_compensations_path())
menu.item("Users", manage_users_path())
end
end%>
that splits out the below string
"<li>Listings</li> <li>Listing changes</li> <li>Flagged Items</li> <li>Transfers</li> <li>Reviews</li> <li>Log</li> <li>Indexer Compensations</li> <li>Users</li>"
I just get this string in my page. I wanted them to be menus nicely styled by CSS. I am just getting the above raw text in my page. How do I convert this string to be treated as HTML by the browser.
Please help
Here is the navigation class
class NavigationMenu < ActionView::Base
def initialize(params)
#params = params
end
def item(title, path, options={})
#items ||= Array.new
unless (route = Rails.application.routes.recognize_path(path,:method => options[:method]|| :get))
raise "Unrecognised path #{path}, are you sure it's in routes.rb?"
end
#items << content_tag(:li, link_to(title,path, :class => (#params[:controller] == route[:controller] && #params[:action] == route[:action])? 'active' : nil))
end
def output
return '' if #items.blank?
content_tag(:ul, #items.join("\n"), :id => 'navigation')
end
end
class Navigation
def self.with(params, &block)
menu = NavigationMenu.new(params)
yield menu
menu.output
end
end
You have to add a call to the raw method:
<%= raw ... %>
This is necessary, because in Rails 3 every string is escaped by default, unless you use the raw method.
It's like an inverse of the h method in Rails 2, where every string is unescaped by default, unless you use the h method.
Example:
This code in Rails 2...
<%= h "String which must be escaped" %>
<%= "String which must be output raw %>
... must be this in Rails 3:
<%= "String which must be escaped" %>
<%= raw "String which must be output raw %>
(Although an additional call to h doesn't do any harm in Rails 3)
You need to append .html_safe to the string - this will stop rails from escaping it when it's time to output text. Probably best to put it in the item method that you call repeatedly.
I recently wrote an article regarding XSS protection in Rails 3 when upgrading from Rails 2:
http://developer.uservoice.com/entries/upgrading-to-rails-3-printing-escaped-strings
The idea is to hook code to printing HTML so that we can determine when we are actually printing something we don't want to:
module ActionView
module Helpers
module TextHelper
def simple_format_with_double_escape_reporting(*args)
HtmlDoubleEscapeReporter.assert_sane(simple_format_without_double_escape_reporting(*args))
end
alias_method_chain :simple_format, :double_escape_reporting
end
module TagHelper
private
def content_tag_string_with_double_escape_reporting(*args)
HtmlDoubleEscapeReporter.assert_sane(content_tag_string_without_double_escape_reporting(*args))
end
alias_method_chain :content_tag_string, :double_escape_reporting
end
module UrlHelper
def link_to_with_double_escape_reporting(*args, &block)
HtmlDoubleEscapeReporter.assert_sane(link_to_without_double_escape_reporting(*args, &block))
end
alias_method_chain :link_to, :double_escape_reporting
end
end
end
Method HtmlDoubleEscapeReporter.assert_sane can be written, for example, like this:
class HtmlDoubleEscapeReporter
def self.assert_sane(str)
if (str.match(/<[a-z]/) || str.match(/&(quot|rarr|larr|amp|#)/)) &&
!str.match(/looks something you do not want to print/
send_problem_report('#{str}' looks something you do not want to print")
end
return str
end
end
Here, 'looks something you do not want to print' is used to prevent the possibility of infinite loops. The line send_problem_report('#{str}' looks something you do not want to print") can be replaced with a call to "debugger" (from ruby-debug gem) so that you are able to check the backtrace and see where the problem is coming from.
Here is the new class. At last... I got that bug.
class NavigationMenu < ActionView::Base
def initialize(params)
#params = params
end
def item(title, path, options={})
#items ||= Array.new
unless (route = Rails.application.routes.recognize_path(path,:method => options[:method]|| :get))
raise "Unrecognised path #{path}, are you sure it's in routes.rb?"
end
#items << content_tag(:li, link_to(title,path, :class => (#params[:controller] == route[:controller] && #params[:action] == route[:action])? 'active' : nil))
end
def output
#items = #items.join("\n").html_safe
return '' if #items.blank?
content_tag(:ul, #items, :id => 'navigation')
end
end
class Navigation
def self.with(params, &block)
menu = NavigationMenu.new(params)
yield menu
menu.output
end
end

Resources