Building a Rails scope using `tap` - ruby-on-rails

I have a method that looks something like
class Student < ActiveRecord::Base
def self.search(options = {})
all.tap do |s|
s.where(first_name: options[:query]) if options[:query]
s.where(graduated: options[:graduated]) if options[:graduated]
# etc there are many more things that can be filtered on...
end
end
end
When calling this method though, I am getting back all of the results and not a filtered set as I would expect. It seems like my tap functionality is not working as I expect. What is the correct way to do this (without assigning all to a variable. I would like to use blocks here if possible).

tap will not work for this.
all is an ActiveRecord::Relation, a query waiting to happen.
all.where(...) returns a new ActiveRecord::Relation the new query.
However checking the documentation for tap, you see that it returns the object that it was called on (in this case all) as opposed to the return value of the block.
i.e. it is defined like this:
def tap
yield self # return from block **discarded**
self
end
When what you wanted was just:
def apply
yield self # return from block **returned**
end
Or something similar to that.
This is why you keep getting all the objects returned, as opposed to the objects resulting from the query. My suggestion is that you build up the hash you send to where as opposed to chaining where calls. Like so:
query = {}
query[:first_name] = options[:query] if options[:query]
query[:graduated] = options[:graduated] if options[:graduated]
# ... etc.
all.where(query)
Or a possibly nicer implementation:
all.where({
first_name: options[:query],
graduated: options[:graduated],
}.delete_if { |_, v| v.empty? })
(If intermediate variables are not to your taste.)

You can easily create a let function:
class Object
def let
return yield self
end
end
And use it like this:
all.let do |s|
s=s.where(first_name: options[:query]) if options[:query]
s=s.where(graduated: options[:graduated]) if options[:graduated]
# etc there are many more things that can be filtered on...
s
end
The difference between tap and let is that tap returns the object and let returns the blocks return value.

These days (ruby >= 2.5) you can use Object.yield_self:
def self.search(options = {})
all.yield_self do |s|
s = s.where(first_name: options[:query]) if options[:query]
s = s.where(graduated: options[:graduated]) if options[:graduated]
s
end
end

Related

Interpolating an attribute's key before save

I'm using Rails 4 and have an Article model that has answer, side_effects, and benefits as attributes.
I am trying to create a before_save method that automatically looks at the side effects and benefits and creates links corresponding to another article on the site.
Instead of writing two virtually identical methods, one for side effects and one for benefits, I would like to use the same method and check to assure the attribute does not equal answer.
So far I have something like this:
before_save :link_to_article
private
def link_to_article
self.attributes.each do |key, value|
unless key == "answer"
linked_attrs = []
self.key.split(';').each do |i|
a = Article.where('lower(specific) = ?', i.downcase.strip).first
if a && a.approved?
linked_attrs.push("<a href='/questions/#{a.slug}' target=_blank>#{i.strip}</a>")
else
linked_attrs.push(i.strip)
end
end
self.key = linked_attrs.join('; ')
end
end
end
but chaining on the key like that gives me an undefined method 'key'.
How can I go about interpolating in the attribute?
in this bit: self.key you are asking for it to literally call a method called key, but what you want, is to call the method-name that is stored in the variable key.
you can use: self.send(key) instead, but it can be a little dangerous.
If somebody hacks up a new form on their browser to send you the attribute called delete! you don't want it accidentally called using send, so it might be better to use read_attribute and write_attribute.
Example below:
def link_to_article
self.attributes.each do |key, value|
unless key == "answer"
linked_attrs = []
self.read_attribute(key).split(';').each do |i|
a = Article.where('lower(specific) = ?', i.downcase.strip).first
if a && a.approved?
linked_attrs.push("<a href='/questions/#{a.slug}' target=_blank>#{i.strip}</a>")
else
linked_attrs.push(i.strip)
end
end
self.write_attribute(key, linked_attrs.join('; '))
end
end
end
I'd also recommend using strong attributes in the controller to make sure you're only permitting the allowed set of attributes.
OLD (before I knew this was to be used on all attributes)
That said... why do you go through every single attribute and only do something if the attribute is called answer? why not just not bother with going through the attributes and look directly at answer?
eg:
def link_to_article
linked_attrs = []
self.answer.split(';').each do |i|
a = Article.where('lower(specific) = ?', i.downcase.strip).first
if a && a.approved?
linked_attrs.push("<a href='/questions/#{a.slug}' target=_blank>#{i.strip}</a>")
else
linked_attrs.push(i.strip)
end
end
self.answer = linked_attrs.join('; ')
end

Rails Handling Nil in Class Method Chaining

I have the following Search class method that takes a bunch of params and then builds a request.
def self.search(agent, params)
RealPropertySale.where(id: available_ids)
.joins(:address)
.by_state(state)
.by_suburb(suburb)
.by_post_code(post_code)
.by_country(country)
.paginate(page: page)
end
def self.by_state(state)
where(addresses: {state: state})
end
def self.by_suburb(suburb)
where(addresses: {suburb: suburb})
end
def self.by_post_code(post_code)
where(addresses: {post_code: post_code})
end
def self.by_country(country)
where(addresses: {country: country})
end
What is the correct way of handling if one of my custom class methods e.g. self.by_country(country) returns nil so that the query continues with whatever param/s are present. I have tried returning self if one the params is blank but the query is lost and the class is returned resulting in errors.
I agree with #Michael Gaskill that you probably should only call the scopes that actually affect the final query (i.e. that have meaningful params).
However, if you insist on the scopes ignoring nil parameters, you may make them return current_scope instead (which is an undocumented but useful method):
def self.by_state(state)
return current_scope if state.nil?
where(addresses: {state: state})
end
We did something similar by breaking it down like so :
response = RealPropertySale.where(id: available_ids)
.joins(:address)
response = response.by_state(state) if state
response = response.by_suburb(suburb) if suburb
response = response.by_post_code(post_code) if post_code
response = response.by_country(country) if country
response = response.paginate(page: page) if page
I like the readibility. I try to break it down to as many pieces as needed, but this should be adapted to your business logic. Don't know if, for you, it makes sense to check if suburb is provided for example.

Interpolate Method Definition

This method does not have a description on the APIdock. I know instance_exec in Ruby is similar to the this binding mechanism in JavaScript.
def interpolate(sql, record = nil)
if sql.respond_to?(:to_proc)
owner.instance_exec(record, &sql)
else
sql
end
end
Could someone briefly describe it?
First of all, the check for respond_to?(:to_proc) is necessary to make sure sql might be converted to lambda (by ampersand & to be passed to instance_exec. To simplify things, one might treat sql here as being a lambda already:
def interpolate(sql, record = nil) # assume sql is lambda
owner.instance_exec(record, &sql)
end
As by documentation on instance_exec:
Executes the given block within the context of the receiver...
That said, lambda will be executed as it was the ordinal code, placed somewhere inside instance method of the receiver.
class Owner
def initialize
#records = [:zero, :one, :two]
end
end
record_by_index = ->(idx) { #records[idx] }
Owner.new.instance_exec 1, &record_by_index #⇒ :one
The code above is [more or less] an equivalent to:
class Owner
def initialize
#records = [:zero, :one, :two]
end
def record_by_index idx
#records[idx]
end
end
Owner.new.record_by_index(1) #⇒ :one
The actual parameters of call to instance_exec will be passed to the codeblock. In the context of Owner’s instance we have an access to instance variables, private methods, etc. Hope it helps.

Optional time where clause

I have an optional time where clause.
Namely where('created_at < ?', params[:infinite_scroll_time_buffer]).
This is included in a series of calls.
I realized that where could take a hash, and if the hash is empty, or is missing any attributes, they won't be included. This sounds great, as I could avoid checking if the params[:infinite_scroll_time_buffer] is there, and just include the where clause and let Rails take care of the rest.
The problem is the following:
def action
options = {}
options[:created_at] = params[:infinite_scroll_time_buffer]
Post.method.another_method.where(options).another_method
end
That would work, except the SQL Query checks that post.created_at = ? instead of post.created_at < ? (rightfully so).
I could have a range of times, but I can't for the life of me find a way for Time to reference the beginning of all time, or something like Time::THE_BEGINNING_OF_EVERYTHING_AS_WE_KNOW_IT_DUN_DUN_DUN
so that I could then have a range from that to the params[:infinite_scroll_time_buffer]. Is there another way to accomplish this?
Create a scope inside your post.rb:
scope :created_before, ->(time) { where('created_at < ?', time) }
Now:
def action
options = {}
Post.method.another_method.where(options).
created_before(params[:infinite_scroll_time_buffer]).
another_method
end
If you don't have the params[:infinite_scroll_time_buffer] present then don't make the query in the first place:
def action
options = {}
# more operation on options hash here
posts = Post.method.another_method.where(options)
posts = posts.created_before(params[:infinite_scroll_time_buffer]) if params[:infinite_scroll_time_buffer].present?
posts = posts.some_another_method
end

Spree error when using decorator with the original code

Need a little help over here :-)
I'm trying to extend the Order class using a decorator, but I get an error back, even when I use the exactly same code from source. For example:
order_decorator.rb (the method is exactly like the source, I'm just using a decorator)
Spree::Order.class_eval do
def update_from_params(params, permitted_params, request_env = {})
success = false
#updating_params = params
run_callbacks :updating_from_params do
attributes = #updating_params[:order] ? #updating_params[:order].permit(permitted_params).delete_if { |k,v| v.nil? } : {}
# Set existing card after setting permitted parameters because
# rails would slice parameters containg ruby objects, apparently
existing_card_id = #updating_params[:order] ? #updating_params[:order][:existing_card] : nil
if existing_card_id.present?
credit_card = CreditCard.find existing_card_id
if credit_card.user_id != self.user_id || credit_card.user_id.blank?
raise Core::GatewayError.new Spree.t(:invalid_credit_card)
end
credit_card.verification_value = params[:cvc_confirm] if params[:cvc_confirm].present?
attributes[:payments_attributes].first[:source] = credit_card
attributes[:payments_attributes].first[:payment_method_id] = credit_card.payment_method_id
attributes[:payments_attributes].first.delete :source_attributes
end
if attributes[:payments_attributes]
attributes[:payments_attributes].first[:request_env] = request_env
end
success = self.update_attributes(attributes)
set_shipments_cost if self.shipments.any?
end
#updating_params = nil
success
end
end
When I run this code, spree never finds #updating_params[:order][:existing_card], even when I select an existing card. Because of that, I can never complete the transaction using a pre-existent card and bogus gateway(gives me empty blanks errors instead).
I tried to bind the method in order_decorator.rb using pry and noticed that the [:existing_card] is actuality at #updating_params' level and not at #updating_params[:order]'s level.
When I delete the decorator, the original code just works fine.
Could somebody explain to me what is wrong with my code?
Thanks,
The method you want to redefine is not really the method of the Order class. It is the method that are mixed by Checkout module within the Order class.
You can see it here: https://github.com/spree/spree/blob/master/core/app/models/spree/order/checkout.rb
Try to do what you want this way:
Create file app/models/spree/order/checkout.rb with code
Spree::Order::Checkout.class_eval do
def self.included(klass)
super
klass.class_eval do
def update_from_params(params, permitted_params, request_env = {})
...
...
...
end
end
end
end

Resources