Monkey patching ActiveRecord "where" method - ruby-on-rails

I'm trying to add some additional functionality to where method in ActiveRecord. I reached half way by doing monkey patch but facing problem with chain queries.
Ex:
User.where(id: 10, name: 'Blob')
When I execute above code my new functionality is working as expected.
It triggers modified where method and gives query params as
query_params = { id: 10, name: 'Blob' }
User.where(id: 10).where(name: 'Blob')
In this case i'm getting only
query_params = { id: 10 }
I'm just printing the query params and delegating to super class:
def where(query, *values)
query.each do |key, value|
if value.is_a?(Array) and value.flatten != value
Rails.logger.debug "Where Clause Params"
Rails.logger.debug "#{query}"
Rails.logger.debug "#{caller.join("\n")}"
break
end
end
super
end
This is how I extend modified where file into ApplicationRecord
ApplicationRecord.extend CoreExtensions::ApplicationRecord::WhereLog
It would be great if anyone helps.

Related

How to write test Rspec for class method?

I'm new to RSpec so this really give me a headache. I have a search by keyword or by category in Post model:
def self.search(search, category_id)
if search.strip.empty?
[]
elsif category_id.empty?
Post.approved.where('lower(title) LIKE ?', "%#{search.downcase.strip}%")
else
#category = Category.find_by('id = ?', category_id.to_i)
#category.posts.approved.where('lower(title) LIKE ?', "%#{search.downcase.strip}%")
end
end
I know how to write test for easy thing like validations and associations, but I still can not figure how to write test for this class method. Any help is appreciated.
You could create some test data:
let!(:post_1) { Post.create(title: 'some example title') }
let!(:post_2) { Post.create(title: 'another title') }
And validate that your search returns the correct records for various search terms, e.g.:
expect(Post.search('example')).to contain_exactly(post_1)
expect(Post.search('EXAMPLE')).to contain_exactly(post_1)
expect(Post.search('title')).to contain_exactly(post_1, post_2)
expect(Post.search('foo')).to be_empty
(assuming search is a method of Post)

How a chaining interface like Rails ActiveRecord is built?

I'm intrigued. How User.where(name: 'Foo').where(age: 20) works with only one database call?
Does the method where know if it is the last of the chain and change its behavior? And if so, how that can be done?
It returns self after each time it adds the query to its internal query builder.
where doesn't necessarily know where it's at; ActiveRecord waits for itself to be enumerated on before even querying the database. Example:
users = User.where(active: true)
users.loaded? # false
users.each { }
users.loaded? # true
each, map, first, last, etc. all will trigger the query to be loaded.
Here's an example of a super-naive query builder:
class FakeRecord
include Enumerable
def self.all_args
#all_args ||= []
end
def self.where(*args)
all_args << args
self
end
def self.each
puts "Executing sql #{all_args.join(", ")}"
yield [1, 2, 3]
end
end
FakeRecord.where(potato: true).where(dinosaur: false).each do |thing|
puts thing
end

RSpec testing model method

I have this method in my models/images.rb model. I am starting with testing and having a hard time coming up with tests for it. Would appreciate your help.
def self.tags
t = "db/data.csv"
#arr = []
csvdata = CSV.read(t)
csvdata.shift
csvdata.each do |row|
row.each_with_index do |l, i|
unless l.nil?
#arr << l
end
end
end
#arr
end
First off a word of advice - CSV is probably the worst imaginable data format and is best avoided unless absolutely unavoidable - like if the client insists that manipulating data in MS Excel is a good idea (it is not).
If you have to use CSV don't use a method name like .tags which can confused for a regular ActiveRecord relation.
Testing methods that read from the file system can be quite difficult.
To start with you might want to alter the signature of the method so that you can pass a file path.
def self.tags(file = "db/data.csv")
# ...
end
That way you can pass a fixture file so that you can test it deterministically.
RSpec.describe Image do
describe "tags" do
let(:file) { Rails.root.join('spec', 'support', 'fixtures', 'tags.csv') }
it 'returns an array' do
expect(Image.tags(file)).to eq [ { foo: 'bar' }, { foo: 'baz' } ]
end
end
end
However your method is very ideosyncratic -
def self.tags
t = "db/data.csv"
#arr = []
self.tags makes it a class method yet you are declaring #arr as an instance variable.
Additionally Ruby's enumerable module provides so many methods for manipulating arrays that using an outer variable in a loop is not needed.
def self.tags(file = "db/data.csv")
csv_data = CSV.read(file)
csv_data.shift
csv_data.compact # removes nil elements
end

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 - Pass collection to ActiveModel object

I am using rails to make a datatable that paginates with Ajax, and I am following railscast #340 to do so.
This episode makes use of a normal ActiveModel Class called ProductsDatatable or in my case OrdersDatatable to create and configure the table. My question has to do with ruby syntax in this class. I am trying to pass a collection of orders to the OrdersDatatable object, from the controller. I want to access this collection in the fetch_orders method.
I create the table object like this in order.rb:
#datatable = OrdersDatatable.new(view_context)
#datatable.shop_id = #current_shop.id
#datatable.orders_list = #orders # which is Order.in_process
And my OrdersDatatable class looks like this: (the important parts which probably need to change is the second line in initialize and the first line in fetch_orders)
class OrdersDatatable
include Rails.application.routes.url_helpers
include ActionView::Helpers::DateHelper
include ActionView::Helpers::TagHelper
delegate :params, :h, :link_to, :number_to_currency, to: :#view
attr_accessor :shop_id, :orders_list
def initialize(view)
#view = view
#orders_list = self.orders_list
end
def current_shop
Shop.find(shop_id)
end
def as_json(options = {})
{
sEcho: params[:sEcho].to_i,
iTotalRecords: orders.count,
iTotalDisplayRecords: orders.count,
aaData: data
}
end
private
def data
orders.map do |order|
[
order.id,
order.name,
h(time_tag(order.date_placed.in_time_zone)),
order.state,
order.source,
order.payment_status,
h(order.delivered? ? 'shipped' : 'unshipped'),
h(number_to_currency order.final_total, unit: order.currency.symbol),
h(link_to 'details', edit_admin_shop_order_path(current_shop, order)),
h(link_to 'delete', admin_shop_order_path(current_shop, order), method: :delete, data: { confirm: 'Are you sure?' } ),
]
end
end
def orders
#orders ||= fetch_orders
end
def fetch_orders
orders = orders_list.order("#{sort_column} #{sort_direction}")
orders = orders.page(page).per_page(per_page)
if params[:sSearch].present?
orders = orders.where("title like :search", search: "%#{params[:sSearch]}%")
end
orders
end
def page
params[:iDisplayStart].to_i/per_page + 1
end
def per_page
params[:iDisplayLength].to_i > 0 ? params[:iDisplayLength].to_i : 10
end
def sort_column
columns = %w[id name date_placed state source payment_status delivered final_total]
columns[params[:iSortCol_0].to_i]
end
def sort_direction
params[:sSortDir_0] == "desc" ? "desc" : "asc"
end
end
When I change the first line in fetch_orders to this
orders = Order.in_process.order("#{sort_column} #{sort_direction}")
which is the hard-coded equivalent, it does work. So I just need the correct syntax
Short answer: If you've got an array, and want to sort it, use the sort_by method:
orders = orders_list.sort_by{|order| "#{order.sort_column} #{order.sort_direction}"}
Long answer: The reason your original code doesn't work is that in this case
Order.in_process.order("#{sort_column} #{sort_direction}")
you are building a query. in_process is a named scope (passing in some conditions), and .order tells rails what to order the query by. Then, when it runs out of chained methods, the query executes (runs some sql) and gets the records out of the DB to build a collection of objects.
Once you are working with a collection of objects, you can't call the .order method on it, as that's just used to assemble an sql query. You need to use Array#sort_by instead. sort_by takes a code block, into which is passed each object in the collection (as order in my example but you could call it anything, it's just a variable name).
BTW, if you just want to call a method on all the objects to sort them, you can use a "shortcut syntax" like .sort_by(&:methodname). This uses a little trick of ruby called Symbol#to_proc (http://railscasts.com/episodes/6-shortcut-blocks-with-symbol-to-proc).
So, for example, if there was a method in Order like so
def sort_string
"#{self.sort_column} #{self.sort_direction}"
end
then you could change your code to
orders = orders_list.sort_by(&:sort_string)
which is neat.
If you have an array, then you can sort like this.
orders = orders_list.sort! {|a,b| a.sort_column <=> b.sort_direction}

Resources