Passing the parameter of an Object into an Object method - ruby

This a simplified version of what I am trying to solve:
In Ruby 2.0.0, Rails 4.0.0, Activerecord
Segment has_many Sales
Sale.find(1).bid = 1
Sale.find(1).ask = 2
Sale.find(2).bid = 10
Sale.find(2).ask = 20
etc
When I write this method:
class Segment
def add_stuff(param)
sales.map{ |s| s.param }.inject(:+)
end
end
Question: How to pass in bid or ask as param and interpolate that param properly within the block? Thanks.

Never use map on active record association, when you can use pluck or other querying method like, for example sum:
def add_staff(param)
sales.sum(param)
end
Just for the completeness, here is how to do it with map - note however that map is much slower and error prone than querying methods:
def add_staff(param)
sales.map {|s| s[param]} # if param is name of the column on sales model.
end
Or more general, where param is name of association or custom (non-column) method
def add_staff(param)
sales.map(&param) # Or map {|s| s.send(param) }
end

Related

Correct way to define virtual attributes on a Model for the keys in a JSON column in rails

In my rails model I have a JSON column which stores some meta information.
This is to be entered bu the user from a form.
Since the keys of the JSON column are not attributes of the model I cannot use them directly in form_for instead I need to define a virtual attribute.
Since this number of virtual attributes could grow to be arbitrarily lengthy I would like to use meta programming to define the attributes.
I did try the answer in this question however when I use the constant in my model I get an error saying that the constant is undefined. So I added the symbols for the keys in an array directly and iterate over them in the module. When I do this I get an error that says stack level too deep.
Please can someone help me out here?
If you are using PostgreSQL specific columns like hstore or json simply use store_accessor instead to generate the accessor methods. Be aware that these columns use a string keyed hash and do not allow access using a symbol.
class Model < ActiveRecord::Base
store_accessor :my_json_column, [ :key_1, :key_2, key_3 ]
end
What it doing under the hood? It has define write\read helper methods:
def store_accessor(store_attribute, *keys)
keys = keys.flatten
_store_accessors_module.module_eval do
keys.each do |key|
define_method("#{key}=") do |value|
write_store_attribute(store_attribute, key, value)
end
define_method(key) do
read_store_attribute(store_attribute, key)
end
end
end
# .....
store
I figured it out. I return the attribute as a key of the JSON column and it works fine now.
# lib/virtuals.rb
module Virtuals
%W(key_1 key_2 key_3).each do |attr|
define_method(attr) do
self.my_json_column[attr]
end
define_method("#{attr}=") do |val|
self.my_json_column[attr] = val
end
end
end
In my Model i just need to include that above module and it works fine in the form_for and updates correctly as well.

Custom to_json for nested complex objects in Ruby

I'm new to Ruby and having a little trouble json. I have inherited my classes with custom made JSONable class, as explained HERE in this answer. I have customized it according to my need, but I couldn't figure out how to make it work with custom nested (complex) objects, according to my requirement. I have following scenario.
First Class:
class Option < JSONable
def IncludeAll=(includeAll) #bool
#includeAll = includeAll
end
def IncludeAddress=(includeAddress) #bool
#includeAddress= includeAddress
end
......
Second Class:
class Search < JSONable
def CustomerId=(customerId)
#customerId = customerId
end
def identifier=(identifier)
#identifier = identifier
end
def Options=(options) #This is expected to be of Class Option, declared above
#options = options
end
Third Class:
class Request < JSONable
def DateTimeStamp=(dateTimeStamp)
#dateTimeStamp = dateTimeStamp
end
def SDKVersion=(sDKVersion)
#sDKVersion = sDKVersion
end
def RequestMessage=(requestMessage) #This is of type Search, declared above
#requestMessage = requestMessage
end
I call it as:
search = Search.new
searchOpts = Options.new
request = Request.new
search.identifier = identifier
searchOpts.IncludeAll = false
searchOpts.IncludeAddress = true
search.Options = searchOpts #setting nested level2 property here
//THE MOST OUTER CLASS OBJECT
request.SDKVersion = "xyz"
request.RequestMessage = search #setting nested level1
My ultimate goal is to send this request object to an API, after converting it to JSON. so i call to_json on request object as:
request.to_json
But here, suggested solution in that post (JSONable) fails in this case, as it can't convert the nested complex objects request.search and request.search.Options to Json.
(gives error: in 'to_json': wrong number of arguments (1 for 0) (ArgumentError)')
What I tried:
class JSONable
def to_json
hash = {}
self.instance_variables.each do |var|
#hash[var] = self.instance_variable_get var #tried to apply following check
if((self.instance_variable_get var).instance_of? Options ||((varVal).instance_of? Search))
varVal = self.instance_variable_get var
hash[var] = varVal.to_json #convert inner object to json
else
hash[var] = self.instance_variable_get var
end
end
hash.to_json
end
.....
This converts the nested model without any problem, but it messes up the 3rd level json. The result is as following:
{"DateTimeStamp":"121212","SDKVersion":"1.5","Culture":"en","RequestMessage":"{\"identifier\":\"851848913\",\"Options\":\"{\\\"IncludeAll\\\":true,\\\"IncludeAssociatedEntities\\\":true,\\\"IncludeAddress\\\":true,\\\"IncludePaymentInstructions\\\":true}\"}"}
And API doesn't respond. It seems as it messes up the boolean variables, which should be something like:
"SearchOption":"{\"IncludeAll\":true,\"IncludeAssociatedEntities\":true,\...
but it gives:
"SearchOption\":\"{\\\"IncludeAll\\\":true,\\\"IncludeAssociatedEntities\\\":true,\\\"Includ...
So the API logic can't cast it to corresponding bool objects anymore. JSON validator also fails to validate this result, i checked online
Questions:
How can I avoid this, and produce valid JSON in this case?
How can I apply generic check to in my JSONable class to check if the object is of some custom class / complex object.
(currently i have checked only for specific classes as:)
if((self.instance_variable_get var).instance_of? Options ||((varVal).instance_of? Search))
Other Info:
It works fine for all complex objects, having no nested objects
API is developed in .NET
I'm not using Rails, its a Ruby console app (I'm new to Ruby)
The answer you referred is dated “Dec 2010.” JSON library is included in ruby stdlib for years already and it perfectly converts Hash instances to json. That said, you just need to construct hashes out of your objects and then call JSON.dump on the resulting hash. I have no idea what JSONable is and you definitely do not need it. Introduce some base class, let’s call it Base:
class Base
def to_h
instance_variables.map do |iv|
value = instance_variable_get(:"##{iv}")
[
iv.to_s[1..-1], # name without leading `#`
case value
when Base then value.to_h # Base instance? convert deeply
when Array # Array? convert elements
value.map do |e|
e.respond_to?(:to_h) ? e.to_h : e
end
else value # seems to be non-convertable, put as is
end
]
end.to_h
end
end
Now just derive your classes from Base to make them respond to to_h, define all your instance variables as you did, and call:
require 'json'
JSON.dump request.to_h # request.to_h.to_json should work as well
The above should produce the nested JSON, hashes are happily converted to json by this library automagically.

Fetching array of data from DB using Rails 3

I want to access multiple columns using Rails 3.But it gave me the following error.
Error:
ArgumentError (wrong number of arguments (2 for 1)):
app/controllers/payments_controller.rb:13:in `check_type'
Check my below code.
payment_controller.rb:
class PaymentsController < ApplicationController
def payment
#payment=Vendor.new
respond_to do |format|
format.html
format.js
end
end
def check_type
if params[:commit]=="submit"
#vendor_type=PaymentVendor.where(:v_name => params[:v_name]).pluck(:type ,:Receipt_No)
#vendor_type.each do |vendor|
end
else
#v_name=Vendor.where(:s_catagory => params[:payment][:s_catagory] ).pluck(:v_name)
end
end
end
Actually i want to retrive data like below format.
#vendor_type=["Receipt_no":"type","Receipt_no":"type",.....]
Once these data will appear,I need how to access row values according to Receipt_No.Please help me to resolve this error.
Thanks to ActiveRecord >= 4 . pluck accepts multiple arguments so in
Rails 4: Your query will work
#vendor_type=PaymentVendor.where(:v_name => params[:v_name]).pluck(:type ,:Receipt_No)
Now as you are using Rails 3 which doesn't support multiple arguments to pluck then we can extend ActiveRecord::Relation itself like this:
put your file under config/initializers
# pluck_all.rb
module ActiveRecord
class Relation
def pluck_all(*args)
args.map! do |column_name|
if column_name.is_a?(Symbol) && column_names.include?(column_name.to_s)
"#{connection.quote_table_name(table_name)}.#{connection.quote_column_name(column_name)}"
else
column_name.to_s
end
end
relation = clone
relation.select_values = args
klass.connection.select_all(relation.arel).map! do |attributes|
initialized_attributes = klass.initialize_attributes(attributes)
attributes.each do |key, attribute|
attributes[key] = klass.type_cast_attribute(key, initialized_attributes)
end
end
end
end
end
Now in your controller you can pass multiple arguments to pluck like this:
# payment_controller.rb:
#vendor_type=PaymentVendor.where(:v_name => params[:v_name]).pluck_all(:type ,:Receipt_No)
Now you can use pluck_all in whole app. Hope this helps ;)
EDIT:
Try below code if plcuk_all not worked:
#vendor_type = PaymentVendor.where(:v_name => params[:v_name]).map{|v|[v.type ,v.Receipt_No]}
Reference for more info: http://meltingice.net/2013/06/11/pluck-multiple-columns-rails/
Your pluck(:type ,:Receipt_No) looks wrong,
pluck have only one argument.
Also your type of data #vendor_type is wrong, Array don't have key, value pair.
Use map like this,
#vendor_type=PaymentVendor.where(:v_name => params[:v_name]).map { |i| [i.Receipt_No] }
In terms of making a rails 3 method that behaves the same as the Rails 4 pluck with multiple columns. This outputs a similar array (rather than a hashed key value collection). This should save a bit of pain if you ever come to upgrade and want to clean up the code.
See this tutorial which outlines a similar method that outputs a hash.
config/initializers/pluck_all.rb
module ActiveRecord
class Relation
def pluck_all(*args)
args.map! do |column_name|
if column_name.is_a?(Symbol) && column_names.include?(column_name.to_s)
"#{connection.quote_table_name(table_name)}.#{connection.quote_column_name(column_name)}"
else
column_name.to_s
end
end
relation = clone
relation.select_values = args
klass.connection.select_all(relation.arel).map! do |attributes|
initialized_attributes = klass.initialize_attributes(attributes)
attributes.map do |key, attribute|
klass.type_cast_attribute(key, initialized_attributes)
end
end
end
end
end
Standing on the shoulders of giants and all

Stub ActiveRecord::Relation with ActiveRecord objects

I'm not testing a Rails app. Just getting that out of the way.
I'm testing a library that connects to a relatively active server, restricting records by timestamp. These returned records change as time goes on, making testing other restrictions more complicated. I need to stub out the ActiveRecord::where method to return my own custom relation with objects I create to meet the criteria I need.
Something like
relation = double(ActiveRecord::Relation)
relation.stub(:[]).and_return( [MyClass.new(...), MyClass.new(...), ...] )
MyClass.stub(:where).and_return( relation )
is what I'd like, but that doesn't work. I need it to be an ActiveRecord::Relation because I need to be able to call ActiveRecord::where and ActiveRecord::select on the object in the code.
Edit 2014-01-28
In lib/call.rb
class Call < ActiveRecord::Base
class << self
def sales start_time, end_time
restricted_records = records(start_time, end_time, :agent_id)
#other code
end
#other methods
private
def records start_time, end_time, *select
# I'm leaving in commented code so you can see why I want the ActiveRecord::Relation object, not an Array
calls = Call.where("ts BETWEEN '#{start_time}' AND '#{end_time}'") #.select(select)
raise calls.inspect
#.to_a.map(&:serializable_hash).map {|record| symbolize(record)}
end
end
end
In spec/call_spec.rb
require 'spec_helper'
require 'call.rb'
describe Call do
let(:period_start) { Time.now - 60 }
let(:period_end) { Time.now }
describe "::sales" do
before do
relation = Call.all
relation.stub(:[]).and_return( [Call.new(queue: "12345")] )
Call.stub(:where).and_return( relation )
end
subject { Call.sales(period_start, period_end) }
it "restricts results to my custom object" do
subject
end
end
end
Output from test:
RuntimeError:
#<ActiveRecord::Relation [ #an array containing all the actual Call records, not my object ]>
ActiveRecord::Relation is a class and :[] is an instance method of that class. You're stubbing a method of the class itself, so it's not going to be invoked by any of the Rails code.
If you want MyClass.where to return a relation with just the :[] stubbed, you'll have to create a Relation instance first, as in:
relation = MyClass.all
relation.stub(:[]).and_return( [MyClass.new(...), MyClass.new(...), ...] )
MyClass.stub(:where).and_return( relation )
However, note that in order to get to your returned array in this context, you'll need to do:
MyClass.where("ignored parameters")["ignored parameters"]
Further, if you subsequently call where on relation, you'll return a new instance of Relation which will no longer be stubbed.
Update 2022
The previous upvoted answer is wholly incorrect since does not work with indexing, .to_a, .first, .last, .any?, .none?, any pretty much every other method.
Instead, you can mock the records contained within a relation by stubbing its records method.
custom_records = ["a", "b", "c"]
relation = Model.all
relation.stub(:records).and_return(custom_records)
allow(Model).to receive(:where).and_return(relation)
# Later ...
records = Model.where('1 + 1 = 2') # content of the query doesn't matter, .where is mocked
records.first # => "a"
records.last # => "c"
records.to_a # => ["a", "b", "c"]
records.any? { |x| x == "b" } # => true
Most of the methods will work, but there are a few exceptions that will need to be stubbed separately.
.count - directly invokes a SELECT COUNT(*) SQL query, which bypasses our records mock. Fix:
relation.stub(:count).and_return(custom_records.count)
.exists? - directly invokes another SQL query, again bypassing our records mock. Fix:
relation.stub(:exists?).and_return(custom_records.present?)
others - There are probably other methods that you might need to stub (depending on if your code uses those methods), you can stub each method as-needed
Furthermore you can mock the return value of a has_many relation (which was my actual use case when googling this question) by doing
allow(record).to receive(:related_records).and_wrap_original do |original, *args, &block|
relation = original.call(*args, &block)
relation.stub(:records).and_return(my_custom_array_of_related_records)
relation
end

ActiveRecord and using reject method

I have a model that fetches all the games from a particular city. When I get those games I want to filter them and I would like to use the reject method, but I'm running into an error I'm trying to understand.
# STEP 1 - Model
class Matches < ActiveRecord::Base
def self.total_losses(cities)
reject{ |a| cities.include?(a.winner) }.count
end
end
# STEP 2 - Controller
#games = Matches.find_matches_by("Toronto")
# GOOD! - Returns ActiveRecord::Relation
# STEP 3 - View
cities = ["Toronto", "NYC"]
#games.total_losses(cities)
# FAIL - undefined method reject for #<Class:0x00000101ee6360>
# STEP 3 - View
cities = ["Toronto", "NYC"]
#games.reject{ |a| cities.include?(a.winner) }.count
# PASSES - it returns a number.
Why does reject fail in my model but not in my view ?
The difference is the object you are calling reject on. In the view, #games is an array of Active Record objects, so calling #games.reject uses Array#reject. In your model, you're calling reject on self in a class method, meaning it's attempting to call Matches.reject, which doesn't exist. You need to fetch records first, like this:
def self.total_losses(cities)
all.reject { |a| cities.include(a.winner) }.count
end

Resources